alp-code 0.9.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/CHANGELOG.md +770 -0
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/alp.config.yaml +5 -0
- package/dist/src/agents/agent-definition.js +28 -0
- package/dist/src/agents/capability-catalog.js +33 -0
- package/dist/src/agents/compaction.js +36 -0
- package/dist/src/agents/errors.js +12 -0
- package/dist/src/agents/librarian.js +38 -0
- package/dist/src/agents/main.js +37 -0
- package/dist/src/agents/memory-grant.js +29 -0
- package/dist/src/agents/model-context.js +70 -0
- package/dist/src/agents/modes.js +134 -0
- package/dist/src/agents/oracle.js +36 -0
- package/dist/src/agents/read-thread.js +38 -0
- package/dist/src/agents/registry.js +238 -0
- package/dist/src/agents/render-identity.js +38 -0
- package/dist/src/agents/review.js +37 -0
- package/dist/src/agents/search.js +37 -0
- package/dist/src/agents/shared/house-rules.js +33 -0
- package/dist/src/agents/shared/principal.js +18 -0
- package/dist/src/agents/shared/voice.js +29 -0
- package/dist/src/agents/titling.js +32 -0
- package/dist/src/agents/types.js +15 -0
- package/dist/src/backend/execution-backend.js +2 -0
- package/dist/src/backend/local-execution-store.js +144 -0
- package/dist/src/backend/local-process-backend.js +533 -0
- package/dist/src/backend/local-supervisor.js +104 -0
- package/dist/src/cli/alp.js +380 -0
- package/dist/src/cli/commands/context.js +203 -0
- package/dist/src/cli/commands/delegate.js +136 -0
- package/dist/src/cli/commands/identity-sync.js +31 -0
- package/dist/src/cli/commands/init.js +184 -0
- package/dist/src/cli/commands/mode.js +22 -0
- package/dist/src/cli/commands/principal.js +114 -0
- package/dist/src/cli/commands/run-main.js +90 -0
- package/dist/src/cli/commands/runtime.js +21 -0
- package/dist/src/cli/mode-preference-store.js +62 -0
- package/dist/src/cli/mode-selector.js +178 -0
- package/dist/src/cli/update-check.js +77 -0
- package/dist/src/context/checkpoint.js +134 -0
- package/dist/src/context/compact-journal.js +153 -0
- package/dist/src/context/compact-payload.js +121 -0
- package/dist/src/context/continuity.js +70 -0
- package/dist/src/context/types.js +2 -0
- package/dist/src/delegation/backend-registry.js +40 -0
- package/dist/src/delegation/delegation-service.js +300 -0
- package/dist/src/delegation/types.js +12 -0
- package/dist/src/execution/execution-policy.js +96 -0
- package/dist/src/execution/execution-service.js +115 -0
- package/dist/src/execution/execution-store.js +78 -0
- package/dist/src/execution/identity-capsule.js +65 -0
- package/dist/src/execution/types.js +12 -0
- package/dist/src/hooks/execution-bridge.js +84 -0
- package/dist/src/index.js +4 -0
- package/dist/src/memory/adapters/markdown-file-store.js +257 -0
- package/dist/src/memory/adapters/memory-api-client.js +2 -0
- package/dist/src/memory/adapters/memory-path-mapper.js +76 -0
- package/dist/src/memory/adapters/remote-api-store.js +25 -0
- package/dist/src/memory/context-ranker.js +21 -0
- package/dist/src/memory/errors.js +58 -0
- package/dist/src/memory/memory-service.js +149 -0
- package/dist/src/memory/memory-store.js +2 -0
- package/dist/src/memory/types.js +2 -0
- package/dist/src/policy/capability-policy.js +29 -0
- package/dist/src/policy/delegation-policy.js +25 -0
- package/dist/src/policy/errors.js +10 -0
- package/dist/src/policy/invariants.js +31 -0
- package/dist/src/policy/memory-policy.js +22 -0
- package/dist/src/policy/policy-engine.js +85 -0
- package/dist/src/policy/types.js +8 -0
- package/dist/src/policy/workspace-policy.js +77 -0
- package/dist/src/principal/principal-profile-store.js +89 -0
- package/dist/src/runtime/adapter-files.js +147 -0
- package/dist/src/runtime/claude-adapter.js +177 -0
- package/dist/src/runtime/codex-adapter.js +169 -0
- package/dist/src/runtime/permission-rules.js +156 -0
- package/dist/src/runtime/render-session-context.js +124 -0
- package/dist/src/runtime/render-task-input.js +33 -0
- package/dist/src/runtime/runtime-adapter.js +2 -0
- package/dist/src/runtime/runtime-preference-store.js +66 -0
- package/dist/src/runtime/runtime-selector.js +178 -0
- package/dist/src/runtime/types.js +2 -0
- package/dist/src/runtime/windows-shim.js +57 -0
- package/dist/src/state-paths.js +49 -0
- package/dist/src/workflow/output-validator.js +27 -0
- package/dist/src/workflow/repair-policy.js +8 -0
- package/dist/src/workflow/types.js +22 -0
- package/dist/src/workflow/workflow-runner.js +81 -0
- package/hooks/compact-record.cjs +109 -0
- package/hooks/session-boot.cjs +112 -0
- package/hooks/session-end.cjs +34 -0
- package/package.json +48 -0
- package/scaffold/memory/INDEX.md +27 -0
- package/scaffold/memory/README.md +76 -0
- package/scaffold/memory/projects/INDEX.md +22 -0
- package/scaffold/memory/projects/PROTOCOL.md +128 -0
- package/scaffold/memory/projects/_template/PROJECT.md +45 -0
- package/scripts/alp.cjs +126 -0
- package/scripts/alp.ps1 +4 -0
- package/scripts/alp.sh +3 -0
- package/scripts/bootstrap.cjs +144 -0
- package/scripts/checkout-release.cjs +30 -0
- package/scripts/delegate.cjs +19 -0
- package/scripts/doctor.cjs +158 -0
- package/scripts/doctor.sh +3 -0
- package/scripts/ensure-state.cjs +22 -0
- package/scripts/lib/cli-link.cjs +375 -0
- package/scripts/lib/codex-role.cjs +18 -0
- package/scripts/lib/delegation/command-runner.cjs +108 -0
- package/scripts/lib/delegation/config.cjs +81 -0
- package/scripts/lib/install-paths.cjs +154 -0
- package/scripts/lib/release-manifest.cjs +42 -0
- package/scripts/lib/semver-lite.cjs +20 -0
- package/scripts/lib/state.cjs +274 -0
- package/scripts/lib/uninstall.cjs +252 -0
- package/scripts/lib/update-check-worker.cjs +21 -0
- package/scripts/lib/update.cjs +395 -0
- package/scripts/run-role.cjs +42 -0
- package/scripts/run-role.ps1 +4 -0
- package/scripts/run-role.sh +3 -0
- package/scripts/sync-project-index.sh +167 -0
- package/skills/agent-memory/SKILL.md +109 -0
- package/skills/alp-debug/SKILL.md +90 -0
- package/skills/alp-debug/references/defense-in-depth.md +118 -0
- package/skills/alp-debug/references/investigation-methodology.md +106 -0
- package/skills/alp-debug/references/log-and-ci-analysis.md +96 -0
- package/skills/alp-debug/references/performance-diagnostics.md +112 -0
- package/skills/alp-debug/references/reporting-standards.md +120 -0
- package/skills/alp-debug/references/root-cause-tracing.md +134 -0
- package/skills/alp-debug/references/systematic-debugging.md +93 -0
- package/skills/alp-debug/references/verification.md +86 -0
- package/skills/alp-debug/scripts/find-polluter.sh +63 -0
- package/skills/alp-debug/scripts/find-polluter.test.md +102 -0
- package/skills/alp-plan/SKILL.md +128 -0
- package/skills/alp-plan/references/archive-workflow.md +77 -0
- package/skills/alp-plan/references/codebase-understanding.md +55 -0
- package/skills/alp-plan/references/output-standards.md +96 -0
- package/skills/alp-plan/references/plan-organization.md +129 -0
- package/skills/alp-plan/references/red-team-personas.md +76 -0
- package/skills/alp-plan/references/red-team-workflow.md +81 -0
- package/skills/alp-plan/references/research-phase.md +57 -0
- package/skills/alp-plan/references/scope-challenge.md +82 -0
- package/skills/alp-plan/references/solution-design.md +76 -0
- package/skills/alp-plan/references/validate-question-framework.md +89 -0
- package/skills/alp-plan/references/validate-workflow.md +83 -0
- package/skills/alp-predict/SKILL.md +98 -0
- package/skills/alp-scenario/SKILL.md +86 -0
- package/skills/code-review/SKILL.md +111 -0
- package/skills/code-review/references/code-review-reception.md +114 -0
- package/skills/code-review/references/edge-case-scouting.md +78 -0
- package/skills/code-review/references/verification-before-completion.md +117 -0
- package/skills/delegation/SKILL.md +46 -0
- package/skills/docs-seeker/.env.example +15 -0
- package/skills/docs-seeker/SKILL.md +87 -0
- package/skills/docs-seeker/package.json +25 -0
- package/skills/docs-seeker/references/advanced.md +82 -0
- package/skills/docs-seeker/references/context7-patterns.md +68 -0
- package/skills/docs-seeker/references/errors.md +72 -0
- package/skills/docs-seeker/scripts/analyze-llms-txt.js +211 -0
- package/skills/docs-seeker/scripts/detect-topic.js +172 -0
- package/skills/docs-seeker/scripts/fetch-docs.js +213 -0
- package/skills/docs-seeker/scripts/tests/run-tests.js +72 -0
- package/skills/docs-seeker/scripts/tests/test-analyze-llms.js +119 -0
- package/skills/docs-seeker/scripts/tests/test-detect-topic.js +112 -0
- package/skills/docs-seeker/scripts/tests/test-fetch-docs.js +84 -0
- package/skills/docs-seeker/scripts/utils/env-loader.js +94 -0
- package/skills/docs-seeker/workflows/library-search.md +73 -0
- package/skills/docs-seeker/workflows/repo-analysis.md +90 -0
- package/skills/docs-seeker/workflows/topic-search.md +69 -0
- package/skills/git/SKILL.md +121 -0
- package/skills/git/references/branch-management.md +90 -0
- package/skills/git/references/commit-standards.md +82 -0
- package/skills/git/references/gh-cli-guide.md +132 -0
- package/skills/git/references/safety-protocols.md +86 -0
- package/skills/git/references/workflow-commit.md +89 -0
- package/skills/git/references/workflow-merge.md +63 -0
- package/skills/git/references/workflow-pr.md +70 -0
- package/skills/git/references/workflow-push.md +62 -0
- package/skills/gkg/SKILL.md +87 -0
- package/skills/gkg/references/cli-commands.md +92 -0
- package/skills/gkg/references/http-api.md +99 -0
- package/skills/gkg/references/language-support.md +54 -0
- package/skills/problem-solving/SKILL.md +86 -0
- package/skills/problem-solving/references/attribution.md +48 -0
- package/skills/problem-solving/references/collision-zone-thinking.md +71 -0
- package/skills/problem-solving/references/inversion-exercise.md +88 -0
- package/skills/problem-solving/references/meta-pattern-recognition.md +80 -0
- package/skills/problem-solving/references/scale-game.md +82 -0
- package/skills/problem-solving/references/simplification-cascades.md +83 -0
- package/skills/problem-solving/references/when-stuck.md +76 -0
- package/skills/repomix/SKILL.md +94 -0
- package/skills/repomix/references/configuration.md +134 -0
- package/skills/repomix/references/usage-patterns.md +106 -0
- package/skills/repomix/scripts/.coverage +0 -0
- package/skills/repomix/scripts/README.md +179 -0
- package/skills/repomix/scripts/repomix_batch.py +455 -0
- package/skills/repomix/scripts/repos.example.json +15 -0
- package/skills/repomix/scripts/requirements.txt +15 -0
- package/skills/repomix/scripts/tests/test_repomix_batch.py +531 -0
- package/skills/research/SKILL.md +107 -0
- package/skills/security-scan/SKILL.md +101 -0
- package/skills/security-scan/references/secret-patterns.md +75 -0
- package/skills/security-scan/references/vulnerability-patterns.md +136 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// uninstall.cjs — gỡ cài đặt alp-code nhưng không làm mất memory âm thầm.
|
|
2
|
+
//
|
|
3
|
+
// Mặc định:
|
|
4
|
+
// · gỡ config alp init khỏi mọi workspace còn tồn tại
|
|
5
|
+
// · chuyển memory ra một backup cạnh `~/.alp`
|
|
6
|
+
// · gỡ bản cài theo đúng cách của channel đó, gỡ shim/symlink `alp` và PATH do installer thêm
|
|
7
|
+
// · xoá state của alp-code trong `~/.alp` — theo DANH SÁCH TÊN, không xoá cả thư mục
|
|
8
|
+
// · giữ trust Claude/Codex: entry có thể đã tồn tại trước alp-code và để lại thì vô hại
|
|
9
|
+
//
|
|
10
|
+
// `--purge-memory` do alp.cjs chuyển vào mới cho phép memory bị xoá.
|
|
11
|
+
//
|
|
12
|
+
// Hai điều thay đổi từ v0.9.0, và cả hai đều là chuyện mất dữ liệu nếu làm sai:
|
|
13
|
+
//
|
|
14
|
+
// 1. Memory không còn nằm trong thư mục cài, mà ở `~/.alp/memory`. Xoá thư mục cài không
|
|
15
|
+
// còn đụng tới nó — nhưng xoá `~/.alp` thì đụng, nên chỗ đó phải xoá có chọn lọc.
|
|
16
|
+
// 2. Thư mục cài không còn luôn là một git clone. Bản npm phải để npm gỡ; `rm -rf` sau
|
|
17
|
+
// lưng npm để lại một entry ma trong registry global của nó.
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const { spawnSync } = require("child_process");
|
|
22
|
+
const CLI = require("./cli-link.cjs");
|
|
23
|
+
const P = require("./install-paths.cjs");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Những gì trong `~/.alp` thuộc về alp-code.
|
|
27
|
+
*
|
|
28
|
+
* Danh sách tên chứ không phải `rm -rf` cả thư mục: `~/.alp` là nhà chung, và một lệnh
|
|
29
|
+
* uninstall không có quyền xoá thứ nó không tạo ra. `memory` cố tình vắng mặt ở đây — nó đi
|
|
30
|
+
* đường riêng qua backup hoặc `--purge-memory`.
|
|
31
|
+
*/
|
|
32
|
+
const OWNED_STATE = [
|
|
33
|
+
"agents",
|
|
34
|
+
"hooks",
|
|
35
|
+
"executions",
|
|
36
|
+
"delegation",
|
|
37
|
+
"install.json",
|
|
38
|
+
"update-check.json",
|
|
39
|
+
"mode.json",
|
|
40
|
+
"projects.json",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function uninstall(repoRoot, opts = {}) {
|
|
44
|
+
repoRoot = path.resolve(repoRoot);
|
|
45
|
+
const env = opts.env || process.env;
|
|
46
|
+
const platform = opts.platform || process.platform;
|
|
47
|
+
const cwd = path.resolve(opts.cwd || process.cwd());
|
|
48
|
+
const purgeMemory = !!opts.purgeMemory;
|
|
49
|
+
const force = !!opts.force;
|
|
50
|
+
const log = [];
|
|
51
|
+
const say = (level, text) => log.push({ level, text });
|
|
52
|
+
|
|
53
|
+
const channel = opts.channel || P.detectChannel(repoRoot);
|
|
54
|
+
const stateHome = opts.stateHome || P.stateHome(env);
|
|
55
|
+
const memory = opts.memoryRoot || P.memoryRoot(env);
|
|
56
|
+
|
|
57
|
+
assertInstallRoot(repoRoot);
|
|
58
|
+
if (isWithin(repoRoot, cwd))
|
|
59
|
+
throw new Error(`cwd đang nằm trong ${repoRoot} — cd ra ngoài thư mục này rồi chạy lại \`alp uninstall\``);
|
|
60
|
+
if (!force) assertGitSafe(repoRoot);
|
|
61
|
+
|
|
62
|
+
// Lấy registry machine-local trước khi xoá state.
|
|
63
|
+
const projects = opts.projectPaths || registeredProjects(repoRoot, platform, env);
|
|
64
|
+
|
|
65
|
+
// Memory đi trước mọi thao tác xoá: nếu bước sau hỏng, dữ liệu đã nằm an toàn ở backup.
|
|
66
|
+
let memoryBackup = null;
|
|
67
|
+
if (!purgeMemory && fs.existsSync(memory)) {
|
|
68
|
+
memoryBackup = nextBackupPath(stateHome, opts.now || new Date());
|
|
69
|
+
fs.renameSync(memory, memoryBackup);
|
|
70
|
+
say("SAVED", `${memory} → ${memoryBackup}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Nếu gỡ bản cài thất bại, trả memory về chỗ cũ: một uninstall dở dang không được để dữ
|
|
74
|
+
// liệu bị tách khỏi một bản cài vẫn còn đang chạy.
|
|
75
|
+
try {
|
|
76
|
+
removeInstall(repoRoot, channel, opts, say);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (memoryBackup && fs.existsSync(memoryBackup) && !fs.existsSync(memory)) {
|
|
79
|
+
try { fs.renameSync(memoryBackup, memory); } catch { /* backup vẫn còn, tên vẫn rõ */ }
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Module đã được Node nạp vào RAM nên vẫn tiếp tục cleanup được sau khi thư mục cài biến mất.
|
|
85
|
+
cleanupProjects(projects, say);
|
|
86
|
+
removeOwnedState(stateHome, say);
|
|
87
|
+
const purged = purgeMemory && fs.existsSync(memory);
|
|
88
|
+
if (purged) fs.rmSync(memory, { recursive: true, force: true });
|
|
89
|
+
try {
|
|
90
|
+
for (const entry of CLI.uninstallCli(repoRoot, { ...opts, env, platform }))
|
|
91
|
+
say(entry.level, entry.text);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
say("WARN", `không gỡ hết CLI/PATH: ${e.message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (purged) say("PURGED", `${memory} đã bị xoá theo yêu cầu --purge-memory`);
|
|
97
|
+
else if (!memoryBackup) say("ABSENT", `không có ${memory} để backup`);
|
|
98
|
+
say("KEEP", "trust Claude/Codex — vô hại và có thể đã tồn tại trước alp-code");
|
|
99
|
+
return { log, memoryBackup, projects, channel };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Gỡ thư mục cài theo đúng cách của channel.
|
|
104
|
+
*
|
|
105
|
+
* Bản npm phải do npm gỡ: `rm -rf` thư mục package để lại entry ma trong registry global,
|
|
106
|
+
* và lần `npm i -g alp-code` sau đó có thể im lặng không làm gì vì npm tin rằng nó đã cài rồi.
|
|
107
|
+
*/
|
|
108
|
+
function removeInstall(repoRoot, channel, opts, say) {
|
|
109
|
+
if (channel === "npm") {
|
|
110
|
+
const run = opts.runCommand || npmCommand;
|
|
111
|
+
const result = run("npm", ["uninstall", "--global", "alp-code"]);
|
|
112
|
+
if (result.error || result.status !== 0)
|
|
113
|
+
throw new Error(`\`npm uninstall -g alp-code\` thất bại: ${(result.stderr || result.error?.message || `exit ${result.status}`).toString().trim()}`);
|
|
114
|
+
say("REMOVED", `bản npm ${repoRoot} (qua npm uninstall -g)`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Tarball: xoá cả nhà `~/.alp-code` — versions/, current, chứ không riêng version đang chạy.
|
|
119
|
+
const home = channel === "tarball" ? tarballHomeOf(repoRoot) : repoRoot;
|
|
120
|
+
try {
|
|
121
|
+
fs.rmSync(home, { recursive: true, force: false });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`không xoá được ${home}: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
say("REMOVED", home);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `<home>/versions/<tag>` hoặc `<home>/current` → `<home>`; layout lạ thì giữ nguyên. */
|
|
129
|
+
function tarballHomeOf(repoRoot) {
|
|
130
|
+
const parent = path.dirname(repoRoot);
|
|
131
|
+
if (path.basename(parent) === "versions") return path.dirname(parent);
|
|
132
|
+
if (path.basename(repoRoot) === "current") return parent;
|
|
133
|
+
return repoRoot;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function npmCommand(command, args) {
|
|
137
|
+
const { spawnSyncCommand } = require("./delegation/command-runner.cjs");
|
|
138
|
+
return spawnSyncCommand(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Xoá phần state của alp-code trong `~/.alp`, đúng từng tên trong OWNED_STATE.
|
|
143
|
+
*
|
|
144
|
+
* Thư mục này là nhà chung — `alp` bản khác, config, và những thứ người dùng tự để vào đều
|
|
145
|
+
* có thể đang ở đó. `rm -rf ~/.alp` là cách nhanh nhất để một lệnh gỡ cài đặt xoá mất thứ
|
|
146
|
+
* không phải của nó.
|
|
147
|
+
*/
|
|
148
|
+
function removeOwnedState(stateHome, say) {
|
|
149
|
+
if (!stateHome || !fs.existsSync(stateHome)) {
|
|
150
|
+
say("ABSENT", "state cục bộ");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const removed = [];
|
|
154
|
+
for (const name of OWNED_STATE) {
|
|
155
|
+
const target = path.join(stateHome, name);
|
|
156
|
+
if (!fs.existsSync(target)) continue;
|
|
157
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
158
|
+
removed.push(name);
|
|
159
|
+
}
|
|
160
|
+
if (removed.length) say("REMOVED", `${stateHome}: ${removed.join(", ")}`);
|
|
161
|
+
else say("ABSENT", `${stateHome} không có state của alp-code`);
|
|
162
|
+
try { if (!fs.readdirSync(stateHome).length) fs.rmdirSync(stateHome); } catch { /* còn thứ của người khác */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assertInstallRoot(repoRoot) {
|
|
166
|
+
if (path.dirname(repoRoot) === repoRoot)
|
|
167
|
+
throw new Error(`từ chối uninstall đường dẫn gốc: ${repoRoot}`);
|
|
168
|
+
for (const required of ["package.json", path.join("scripts", "alp.cjs")]) {
|
|
169
|
+
if (!fs.existsSync(path.join(repoRoot, required)))
|
|
170
|
+
throw new Error(`${repoRoot} không phải một bản cài alp-code (thiếu ${required})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Không xoá một dev clone còn việc chưa commit hoặc commit chưa push. */
|
|
175
|
+
function assertGitSafe(repoRoot) {
|
|
176
|
+
if (!fs.existsSync(path.join(repoRoot, ".git"))) return;
|
|
177
|
+
const status = git(repoRoot, ["status", "--porcelain"]);
|
|
178
|
+
if (status.stdout.trim())
|
|
179
|
+
throw new Error("repo alp-code còn thay đổi chưa commit — xử lý trước, hoặc dùng --force nếu thật sự muốn xoá");
|
|
180
|
+
|
|
181
|
+
const upstream = git(repoRoot, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], true);
|
|
182
|
+
if (upstream.status !== 0) return;
|
|
183
|
+
const ahead = git(repoRoot, ["rev-list", "--count", "@{u}..HEAD"]);
|
|
184
|
+
if (Number(ahead.stdout.trim()) > 0)
|
|
185
|
+
throw new Error("repo alp-code có commit chưa push — push/backup trước, hoặc dùng --force nếu thật sự muốn xoá");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function git(repoRoot, args, allowFailure = false) {
|
|
189
|
+
const r = spawnSync("git", ["-C", repoRoot, ...args], { encoding: "utf8" });
|
|
190
|
+
if (r.error) throw new Error(`không chạy được git: ${r.error.message}`);
|
|
191
|
+
if (!allowFailure && r.status !== 0)
|
|
192
|
+
throw new Error((r.stderr || "").trim() || `git ${args[0]} thất bại (exit ${r.status})`);
|
|
193
|
+
return r;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function registeredProjects(repoRoot, platform, env = process.env) {
|
|
197
|
+
const file = path.join(P.stateHome(env), "projects.json");
|
|
198
|
+
if (!fs.existsSync(file)) return [];
|
|
199
|
+
const projects = JSON.parse(fs.readFileSync(file, "utf8")).projects || [];
|
|
200
|
+
return [...new Map(projects.map((entry) => {
|
|
201
|
+
const resolved = path.resolve(entry.path);
|
|
202
|
+
return [platform === "win32" ? resolved.toLowerCase() : resolved, resolved];
|
|
203
|
+
})).values()];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function cleanupProjects(projects, say) {
|
|
207
|
+
for (const project of projects) {
|
|
208
|
+
if (!fs.existsSync(project)) {
|
|
209
|
+
say("ABSENT", `${project} — workspace không còn trên đĩa`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
for (const file of [path.join(project, ".claude", "settings.local.json"), path.join(project, ".codex", "config.toml")]) {
|
|
214
|
+
if (!fs.existsSync(file)) continue;
|
|
215
|
+
const body = fs.readFileSync(file, "utf8");
|
|
216
|
+
if (!body.toLowerCase().includes("alp init")) { say("KEEP", `${file} — không phải file do alp init sinh`); continue; }
|
|
217
|
+
fs.rmSync(file);
|
|
218
|
+
say("REMOVED", file);
|
|
219
|
+
const backup = `${file}.alp-backup`;
|
|
220
|
+
if (fs.existsSync(backup)) { fs.renameSync(backup, file); say("RESTORED", file); }
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
say("WARN", `${project} — không gỡ hết config cục bộ: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `~/.alp` → `~/.alp.memory-backup-<stamp>`: cạnh state home, không nằm trong thứ vừa bị xoá. */
|
|
229
|
+
function nextBackupPath(anchor, now) {
|
|
230
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
231
|
+
const base = `${anchor}.memory-backup-${stamp}`;
|
|
232
|
+
let candidate = base;
|
|
233
|
+
for (let i = 2; fs.existsSync(candidate); i++) candidate = `${base}-${i}`;
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isWithin(parent, child) {
|
|
238
|
+
const rel = path.relative(parent, child);
|
|
239
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
uninstall,
|
|
244
|
+
assertInstallRoot,
|
|
245
|
+
assertGitSafe,
|
|
246
|
+
registeredProjects,
|
|
247
|
+
nextBackupPath,
|
|
248
|
+
isWithin,
|
|
249
|
+
removeOwnedState,
|
|
250
|
+
tarballHomeOf,
|
|
251
|
+
OWNED_STATE,
|
|
252
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Detached child entrypoint for the background "is there a newer release" check. Spawned by
|
|
3
|
+
// src/cli/update-check.ts with stdio:"ignore" and unref()'d — this process outlives the `alp`
|
|
4
|
+
// invocation that spawned it and must never throw uncaught or hang the caller.
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const { resolveLatestReleaseTag } = require("./update.cjs");
|
|
11
|
+
const { FileUpdateCheckStore } = require(path.join(repoRoot, "dist", "src", "cli", "update-check.js"));
|
|
12
|
+
|
|
13
|
+
process.on("unhandledRejection", () => process.exit(0));
|
|
14
|
+
|
|
15
|
+
(async () => {
|
|
16
|
+
const resolved = await resolveLatestReleaseTag(repoRoot, { timeoutMs: 4000 }).catch(() => ({ ok: false }));
|
|
17
|
+
await new FileUpdateCheckStore()
|
|
18
|
+
.write({ checkedAt: Date.now(), latestTag: resolved.ok ? resolved.tag : null })
|
|
19
|
+
.catch(() => {});
|
|
20
|
+
process.exit(0);
|
|
21
|
+
})();
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// `alp update`, một đường cho mỗi kiểu cài. Kiểu cài quyết định cách cập nhật, và cả ba đường
|
|
2
|
+
// đều KHÔNG build gì trên máy người dùng nữa:
|
|
3
|
+
//
|
|
4
|
+
// npm — `npm install -g alp-code@<version>`; npm lo cả code lẫn dependency.
|
|
5
|
+
// tarball — tải bundle của GitHub Release, giải nén sang `versions/<tag>` rồi trỏ lại
|
|
6
|
+
// `current`. Thư mục cũ còn nguyên tới khi bản mới đứng được.
|
|
7
|
+
// dev — clone của người phát triển: vẫn checkout tag rồi build tại chỗ, vì đó chính là
|
|
8
|
+
// thứ một dev clone dùng để làm việc.
|
|
9
|
+
//
|
|
10
|
+
// Không còn bước backup/restore memory quanh update: từ v0.9.0 memory và mọi state khác nằm ở
|
|
11
|
+
// `~/.alp`, ngoài tầm với của thứ đang bị thay. Chép dữ liệu qua lại quanh một thao tác không
|
|
12
|
+
// đụng tới nó chỉ thêm một đường có thể hỏng.
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { spawnSync } = require("child_process");
|
|
17
|
+
const semver = require("./semver-lite.cjs");
|
|
18
|
+
const P = require("./install-paths.cjs");
|
|
19
|
+
|
|
20
|
+
const REPO_SLUG = "phucanh08/alp-code";
|
|
21
|
+
|
|
22
|
+
function assertCleanWorkingTree(repoRoot, env) {
|
|
23
|
+
const staged = gitLines(repoRoot, ["diff", "--cached", "--name-only"], env);
|
|
24
|
+
if (!staged.ok) return failure(`không kiểm tra được staged changes: ${staged.message}`);
|
|
25
|
+
if (staged.lines.length)
|
|
26
|
+
return failure(`repo có staged changes; không tự cất: ${staged.lines.join(", ")}`);
|
|
27
|
+
|
|
28
|
+
const dirty = gitLines(repoRoot, ["diff", "--name-only"], env);
|
|
29
|
+
if (!dirty.ok) return failure(`không kiểm tra được working tree: ${dirty.message}`);
|
|
30
|
+
|
|
31
|
+
if (dirty.lines.length) return failure(`repo có tracked changes; không tự cất: ${dirty.lines.join(", ")}`);
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolveLatestReleaseTag(repoRoot, options = {}) {
|
|
36
|
+
const repoSlug = options.repoSlug || REPO_SLUG;
|
|
37
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
38
|
+
if (fetchImpl) {
|
|
39
|
+
try {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs || 5000);
|
|
42
|
+
const response = await fetchImpl(`https://api.github.com/repos/${repoSlug}/releases/latest`, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: { "User-Agent": "alp-code-updater" },
|
|
45
|
+
});
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
const body = await response.json();
|
|
49
|
+
if (body && typeof body.tag_name === "string" && semver.isValid(body.tag_name)) {
|
|
50
|
+
return { ok: true, tag: body.tag_name, source: "github-api" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
/* fall through to git-based fallback */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// `git ls-remote` chỉ là lối thoát cho dev clone. Bản cài từ npm hay tarball không có `.git`
|
|
58
|
+
// nào cả, nên hỏi git ở đó chắc chắn hỏng — và hỏng bằng một thông báo của git, che mất lý
|
|
59
|
+
// do thật là không gọi được GitHub API.
|
|
60
|
+
const remote = options.remote || "origin";
|
|
61
|
+
if (!fs.existsSync(path.join(repoRoot, ".git")))
|
|
62
|
+
return { ok: false, message: "không hỏi được GitHub Releases (mạng?) và bản cài này không phải git clone để tra tag" };
|
|
63
|
+
const listed = gitText(repoRoot, ["ls-remote", "--tags", "--refs", remote], options.env || process.env);
|
|
64
|
+
if (!listed.ok) return { ok: false, message: `không lấy được danh sách tag: ${listed.message}` };
|
|
65
|
+
const tags = listed.text
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.map((line) => line.split("refs/tags/")[1])
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.filter((tag) => semver.isValid(tag));
|
|
70
|
+
if (!tags.length) return { ok: false, message: "không tìm thấy tag phiên bản (vX.Y.Z) trên remote" };
|
|
71
|
+
tags.sort(semver.compare);
|
|
72
|
+
return { ok: true, tag: tags[tags.length - 1], source: "git-ls-remote" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Git and npm are chatty, and `alp update` is a one-line errand: say which version you moved
|
|
77
|
+
* to, not every object counted and every package linked. Output is captured by default so a
|
|
78
|
+
* successful run stays quiet — `commandFailure` reads the same captured streams, so a failure
|
|
79
|
+
* still reports exactly what the tool said. `verbose: true` puts the firehose back, which is
|
|
80
|
+
* what the installers want and what a broken build needs.
|
|
81
|
+
*/
|
|
82
|
+
function quietStdio(options) {
|
|
83
|
+
return options.stdio || (options.verbose ? "inherit" : ["ignore", "pipe", "pipe"]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function packageVersion(repoRoot) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function checkoutLatestRelease(repoRoot, options = {}) {
|
|
95
|
+
const env = options.env || process.env;
|
|
96
|
+
const stdio = quietStdio(options);
|
|
97
|
+
const log = options.log || (() => {});
|
|
98
|
+
|
|
99
|
+
const clean = assertCleanWorkingTree(repoRoot, env);
|
|
100
|
+
if (!clean.ok) return clean;
|
|
101
|
+
log("CHECK", "tracked source clean; machine-local project registry is outside git");
|
|
102
|
+
|
|
103
|
+
const remote = options.remote || "origin";
|
|
104
|
+
const fetched = runGit(repoRoot, ["fetch", "--tags", "--force", remote], { env, stdio });
|
|
105
|
+
if (!succeeded(fetched)) return failure(commandFailure("git fetch --tags", fetched));
|
|
106
|
+
|
|
107
|
+
const resolved = options.pinTag
|
|
108
|
+
? { ok: true, tag: options.pinTag.startsWith("v") ? options.pinTag : `v${options.pinTag}`, source: "pinned" }
|
|
109
|
+
: await (options.resolveLatestReleaseTag || resolveLatestReleaseTag)(repoRoot, options);
|
|
110
|
+
if (!resolved.ok) return failure(resolved.message);
|
|
111
|
+
|
|
112
|
+
log("CHECKOUT", `${resolved.tag} (${resolved.source})`);
|
|
113
|
+
const checkedOut = runGit(repoRoot, ["checkout", "--detach", resolved.tag], { env, stdio });
|
|
114
|
+
if (!succeeded(checkedOut)) return failure(commandFailure(`git checkout ${resolved.tag}`, checkedOut));
|
|
115
|
+
return { ok: true, status: 0, tag: resolved.tag, source: resolved.source, preserved: [] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `alp update` — chọn đường theo kiểu cài rồi đi.
|
|
120
|
+
*
|
|
121
|
+
* `options.channel` chỉ để test ép nhánh; ngoài đời kiểu cài đọc được từ chính thư mục cài,
|
|
122
|
+
* vì cùng một cây file đi cả npm lẫn tarball nên chỉ vị trí mới phân biệt được chúng.
|
|
123
|
+
*/
|
|
124
|
+
async function updateInstallation(repoRoot, options = {}) {
|
|
125
|
+
const channel = options.channel || P.detectChannel(repoRoot);
|
|
126
|
+
if (channel === "npm") return updateNpmInstall(repoRoot, options);
|
|
127
|
+
if (channel === "tarball") return updateTarballInstall(repoRoot, options);
|
|
128
|
+
return updateDevClone(repoRoot, options);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Version đích cho một bản cài, sau khi hỏi GitHub Release (hoặc theo `--version` đã ghim). */
|
|
132
|
+
async function resolveTarget(repoRoot, options) {
|
|
133
|
+
const from = packageVersion(repoRoot);
|
|
134
|
+
const resolved = options.pinTag
|
|
135
|
+
? { ok: true, tag: normalizeTag(options.pinTag), source: "pinned" }
|
|
136
|
+
: await (options.resolveLatestReleaseTag || resolveLatestReleaseTag)(repoRoot, options);
|
|
137
|
+
if (!resolved.ok) return { ok: false, message: resolved.message };
|
|
138
|
+
return { ok: true, from, tag: resolved.tag, version: resolved.tag.replace(/^v/, ""), source: resolved.source };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeTag(tag) {
|
|
142
|
+
return String(tag).startsWith("v") ? String(tag) : `v${tag}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Chạy `ensure-state.cjs` bằng một tiến trình MỚI trên thư mục cài MỚI.
|
|
147
|
+
*
|
|
148
|
+
* Không gọi thẳng `ensureState()` trong tiến trình này được: code đang chạy đến từ thư mục vừa
|
|
149
|
+
* bị thay, `require` một file chưa nạp sẽ đọc trúng bản đã bị xoá hoặc bản nửa cũ nửa mới.
|
|
150
|
+
*/
|
|
151
|
+
function runEnsureState(root, options) {
|
|
152
|
+
const spawnProcess = options.spawnProcess || spawnSync;
|
|
153
|
+
const result = spawnProcess(process.execPath, [path.join(root, "scripts", "ensure-state.cjs"), "--quiet"], {
|
|
154
|
+
cwd: root,
|
|
155
|
+
env: options.env || process.env,
|
|
156
|
+
stdio: quietStdio(options),
|
|
157
|
+
});
|
|
158
|
+
return succeeded(result) ? { ok: true } : failure(commandFailure("ensure-state.cjs", result));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ------------------------------------------------------------------ channel npm
|
|
162
|
+
|
|
163
|
+
async function updateNpmInstall(repoRoot, options = {}) {
|
|
164
|
+
const target = await resolveTarget(repoRoot, options);
|
|
165
|
+
if (!target.ok) return failure(target.message);
|
|
166
|
+
if (target.from === target.version && !options.force)
|
|
167
|
+
return { ok: true, tag: target.tag, from: target.from, to: target.from, channel: "npm", unchanged: true };
|
|
168
|
+
|
|
169
|
+
(options.onCheckout || (() => {}))({ from: target.from, tag: target.tag });
|
|
170
|
+
const runCommand = options.runCommand || spawnCommand;
|
|
171
|
+
const installed = runCommand("npm", ["install", "--global", `alp-code@${target.version}`], {
|
|
172
|
+
env: options.env || process.env,
|
|
173
|
+
stdio: quietStdio(options),
|
|
174
|
+
});
|
|
175
|
+
if (!succeeded(installed)) {
|
|
176
|
+
const detail = commandFailure("npm install -g", installed);
|
|
177
|
+
return failure(/EACCES|permission denied/i.test(detail)
|
|
178
|
+
? `npm không ghi được vào thư mục global: ${detail}\n Sửa quyền (npm config set prefix ~/.npm-global) hoặc cài lại bằng installer tarball.`
|
|
179
|
+
: detail);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ensured = runEnsureState(repoRoot, options);
|
|
183
|
+
if (!ensured.ok) return ensured;
|
|
184
|
+
return { ok: true, tag: target.tag, from: target.from, to: packageVersion(repoRoot) || target.version, channel: "npm" };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// -------------------------------------------------------------- channel tarball
|
|
188
|
+
|
|
189
|
+
/** `~/.alp-code` suy từ thư mục cài: `<home>/versions/<tag>` hoặc chính nó nếu layout cũ. */
|
|
190
|
+
function tarballHomeFor(repoRoot, env) {
|
|
191
|
+
const resolved = path.resolve(repoRoot);
|
|
192
|
+
const parent = path.dirname(resolved);
|
|
193
|
+
return path.basename(parent) === "versions" ? path.dirname(parent) : resolved;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function bundleAssetName(tag) {
|
|
197
|
+
return `alp-code-${tag}-bundle.tar.gz`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function bundleUrl(tag, options = {}) {
|
|
201
|
+
return `https://github.com/${options.repoSlug || REPO_SLUG}/releases/download/${tag}/${bundleAssetName(tag)}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function updateTarballInstall(repoRoot, options = {}) {
|
|
205
|
+
const target = await resolveTarget(repoRoot, options);
|
|
206
|
+
if (!target.ok) return failure(target.message);
|
|
207
|
+
if (target.from === target.version && !options.force)
|
|
208
|
+
return { ok: true, tag: target.tag, from: target.from, to: target.from, channel: "tarball", unchanged: true };
|
|
209
|
+
|
|
210
|
+
(options.onCheckout || (() => {}))({ from: target.from, tag: target.tag });
|
|
211
|
+
const home = options.tarballHome ? path.resolve(options.tarballHome) : tarballHomeFor(repoRoot, options.env || process.env);
|
|
212
|
+
const installed = await installBundle(home, target.tag, options);
|
|
213
|
+
if (!installed.ok) return installed;
|
|
214
|
+
|
|
215
|
+
const ensured = runEnsureState(installed.root, options);
|
|
216
|
+
if (!ensured.ok) return ensured;
|
|
217
|
+
pruneVersions(home, [path.basename(installed.root), path.basename(path.resolve(repoRoot))]);
|
|
218
|
+
return { ok: true, tag: target.tag, from: target.from, to: target.version, channel: "tarball", root: installed.root };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Tải bundle về, giải nén sang một thư mục MỚI, rồi mới trỏ `current` sang đó.
|
|
223
|
+
*
|
|
224
|
+
* Thứ tự này là điểm mấu chốt: bản đang chạy không bị đụng tới cho đến khi bản mới đã nằm đủ
|
|
225
|
+
* trên đĩa. Tải dở giữa chừng hay tar hỏng thì `current` vẫn trỏ vào bản cũ và người dùng vẫn
|
|
226
|
+
* còn một `alp` chạy được — thay tại chỗ thì hỏng giữa chừng là mất luôn cả hai.
|
|
227
|
+
*/
|
|
228
|
+
async function installBundle(home, tag, options = {}) {
|
|
229
|
+
const versions = path.join(home, "versions");
|
|
230
|
+
const destination = path.join(versions, tag);
|
|
231
|
+
const staging = path.join(versions, `.incoming-${tag}-${process.pid}`);
|
|
232
|
+
fs.mkdirSync(versions, { recursive: true });
|
|
233
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
234
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
235
|
+
|
|
236
|
+
const archive = path.join(versions, `.${tag}-${process.pid}.tar.gz`);
|
|
237
|
+
try {
|
|
238
|
+
const downloaded = await downloadFile(bundleUrl(tag, options), archive, options);
|
|
239
|
+
if (!downloaded.ok) return failure(downloaded.message);
|
|
240
|
+
|
|
241
|
+
const extracted = (options.runCommand || spawnCommand)("tar", ["-xzf", archive, "-C", staging], {
|
|
242
|
+
stdio: quietStdio(options),
|
|
243
|
+
env: options.env || process.env,
|
|
244
|
+
});
|
|
245
|
+
if (!succeeded(extracted)) return failure(commandFailure("tar -xzf", extracted));
|
|
246
|
+
if (!fs.existsSync(path.join(staging, "scripts", "alp.cjs")))
|
|
247
|
+
return failure(`bundle ${bundleAssetName(tag)} không đúng cấu trúc — thiếu scripts/alp.cjs`);
|
|
248
|
+
|
|
249
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
250
|
+
fs.renameSync(staging, destination);
|
|
251
|
+
} finally {
|
|
252
|
+
fs.rmSync(archive, { force: true });
|
|
253
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const pointed = pointCurrent(home, destination);
|
|
257
|
+
if (!pointed.ok) return pointed;
|
|
258
|
+
return { ok: true, root: destination };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `<home>/current` → version đang dùng, thay bằng rename nên không có khoảnh khắc nào nó
|
|
263
|
+
* không trỏ đi đâu cả. Windows dùng junction: symlink thư mục ở đó cần quyền admin hoặc
|
|
264
|
+
* Developer Mode, junction thì không cần gì.
|
|
265
|
+
*/
|
|
266
|
+
function pointCurrent(home, target) {
|
|
267
|
+
const link = path.join(home, "current");
|
|
268
|
+
const temporary = path.join(home, `.current-${process.pid}`);
|
|
269
|
+
const type = process.platform === "win32" ? "junction" : "dir";
|
|
270
|
+
try {
|
|
271
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
272
|
+
fs.symlinkSync(target, temporary, type);
|
|
273
|
+
fs.rmSync(link, { recursive: true, force: true });
|
|
274
|
+
fs.renameSync(temporary, link);
|
|
275
|
+
return { ok: true, link };
|
|
276
|
+
} catch (error) {
|
|
277
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
278
|
+
return failure(`không trỏ được ${link} sang ${target}: ${error.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Giữ bản đang chạy và bản vừa cài, dọn phần còn lại — đủ để lùi một bước, không hơn. */
|
|
283
|
+
function pruneVersions(home, keep) {
|
|
284
|
+
const versions = path.join(home, "versions");
|
|
285
|
+
let entries;
|
|
286
|
+
try { entries = fs.readdirSync(versions); } catch { return; }
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
if (keep.includes(entry)) continue;
|
|
289
|
+
if (entry.startsWith(".incoming-")) { fs.rmSync(path.join(versions, entry), { recursive: true, force: true }); continue; }
|
|
290
|
+
if (!semver.isValid(entry)) continue;
|
|
291
|
+
fs.rmSync(path.join(versions, entry), { recursive: true, force: true });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function downloadFile(url, destination, options = {}) {
|
|
296
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
297
|
+
if (!fetchImpl) return { ok: false, message: "Node này không có fetch — cần Node >= 18" };
|
|
298
|
+
try {
|
|
299
|
+
const response = await fetchImpl(url, { headers: { "User-Agent": "alp-code-updater" }, redirect: "follow" });
|
|
300
|
+
if (!response.ok) return { ok: false, message: `tải ${url} thất bại (HTTP ${response.status})` };
|
|
301
|
+
fs.writeFileSync(destination, Buffer.from(await response.arrayBuffer()));
|
|
302
|
+
return { ok: true };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { ok: false, message: `tải ${url} thất bại: ${error.message}` };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ------------------------------------------------------------------ channel dev
|
|
309
|
+
|
|
310
|
+
async function updateDevClone(repoRoot, options = {}) {
|
|
311
|
+
const from = packageVersion(repoRoot);
|
|
312
|
+
const checkedOut = await checkoutLatestRelease(repoRoot, options);
|
|
313
|
+
if (!checkedOut.ok) return checkedOut;
|
|
314
|
+
(options.onCheckout || (() => {}))({ from, tag: checkedOut.tag });
|
|
315
|
+
|
|
316
|
+
const spawnProcess = options.spawnProcess || spawnSync;
|
|
317
|
+
const built = spawnProcess(process.execPath, [path.join(repoRoot, "scripts", "bootstrap.cjs"), "--no-path"], {
|
|
318
|
+
cwd: repoRoot,
|
|
319
|
+
env: options.env || process.env,
|
|
320
|
+
stdio: quietStdio(options),
|
|
321
|
+
});
|
|
322
|
+
return succeeded(built)
|
|
323
|
+
? { ...checkedOut, from, to: packageVersion(repoRoot), channel: "dev" }
|
|
324
|
+
: failure(commandFailure("bootstrap.cjs", built));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function spawnCommand(command, args, options) {
|
|
328
|
+
const { spawnSyncCommand } = require("./delegation/command-runner.cjs");
|
|
329
|
+
return spawnSyncCommand(command, args, options);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function onlyWorkspaceChanged(base, current) {
|
|
333
|
+
const normalizedBase = normalizeNewlines(base);
|
|
334
|
+
const normalizedCurrent = normalizeNewlines(current);
|
|
335
|
+
const workspacePattern = /^workspaces:\s*$\n(?:^[ \t]+.*(?:\n|$))*/m;
|
|
336
|
+
if (!workspacePattern.test(normalizedBase) || !workspacePattern.test(normalizedCurrent)) return false;
|
|
337
|
+
return normalizedBase.replace(workspacePattern, "workspaces:\n") ===
|
|
338
|
+
normalizedCurrent.replace(workspacePattern, "workspaces:\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function gitLines(repoRoot, args, env) {
|
|
342
|
+
const result = gitText(repoRoot, args, env);
|
|
343
|
+
return result.ok
|
|
344
|
+
? { ok: true, lines: result.text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) }
|
|
345
|
+
: result;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function gitText(repoRoot, args, env) {
|
|
349
|
+
const result = runGit(repoRoot, args, {
|
|
350
|
+
env,
|
|
351
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
352
|
+
encoding: "utf8",
|
|
353
|
+
});
|
|
354
|
+
return succeeded(result)
|
|
355
|
+
? { ok: true, text: String(result.stdout || "") }
|
|
356
|
+
: { ok: false, message: commandFailure(`git ${args.join(" ")}`, result) };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function runGit(repoRoot, args, options) {
|
|
360
|
+
return spawnSync("git", ["-C", repoRoot, ...args], options);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function commandFailure(command, result) {
|
|
364
|
+
return String(result?.stderr || result?.stdout || result?.error?.message || `${command} exit ${result?.status}`).trim();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function succeeded(result) {
|
|
368
|
+
return !result.error && result.status === 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function failure(message) {
|
|
372
|
+
return { ok: false, status: 1, message, preserved: [] };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function normalizeNewlines(text) {
|
|
376
|
+
return String(text).replace(/\r\n/g, "\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
onlyWorkspaceChanged,
|
|
381
|
+
packageVersion,
|
|
382
|
+
assertCleanWorkingTree,
|
|
383
|
+
resolveLatestReleaseTag,
|
|
384
|
+
checkoutLatestRelease,
|
|
385
|
+
updateInstallation,
|
|
386
|
+
updateNpmInstall,
|
|
387
|
+
updateTarballInstall,
|
|
388
|
+
updateDevClone,
|
|
389
|
+
installBundle,
|
|
390
|
+
pointCurrent,
|
|
391
|
+
pruneVersions,
|
|
392
|
+
tarballHomeFor,
|
|
393
|
+
bundleAssetName,
|
|
394
|
+
bundleUrl,
|
|
395
|
+
};
|