@thieung/agentkit-helper 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.
@@ -0,0 +1,137 @@
1
+ import { homedir } from "node:os";
2
+ import { spawn } from "node:child_process";
3
+
4
+ export const DEFAULT_ISSUE_REPO = "thieung/agentkit-helper";
5
+
6
+ export function resolveIssueRepository(env = process.env) {
7
+ const repository = (env.AK_HELPER_ISSUE_REPO || DEFAULT_ISSUE_REPO).trim();
8
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
9
+ throw new Error("AK_HELPER_ISSUE_REPO must use owner/repository format");
10
+ }
11
+ return repository;
12
+ }
13
+
14
+ export function issueUrl(repository = resolveIssueRepository()) {
15
+ return `https://github.com/${repository}/issues/new`;
16
+ }
17
+
18
+ export function isReportableAkError(error, akBinary = "ak") {
19
+ return error?.command?.binary === akBinary;
20
+ }
21
+
22
+ function replaceAllLiteral(value, needle, replacement) {
23
+ return needle ? value.split(needle).join(replacement) : value;
24
+ }
25
+
26
+ function isRedactablePath(value) {
27
+ return typeof value === "string" && value.length > 1;
28
+ }
29
+
30
+ export function redact(value, { cwd = process.cwd(), home = homedir() } = {}) {
31
+ let result = String(value ?? "");
32
+ if (isRedactablePath(cwd)) {
33
+ result = replaceAllLiteral(result, cwd, "$PROJECT");
34
+ }
35
+ if (isRedactablePath(home)) {
36
+ result = replaceAllLiteral(result, home, "$HOME");
37
+ }
38
+ result = result
39
+ .replace(/\b(?:gh[opusr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+)\b/g, "[REDACTED]")
40
+ .replace(/\b(Bearer)\s+\S+/gi, "$1 [REDACTED]")
41
+ .replace(/\b(token|api[_-]?key|secret|password)=\S+/gi, "$1=[REDACTED]");
42
+ return result;
43
+ }
44
+
45
+ export function buildIssueReport({ error, helperVersion, action, language, cwd = process.cwd() }) {
46
+ const exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : 1;
47
+ const command = error?.command
48
+ ? [error.command.binary, ...error.command.args].join(" ")
49
+ : "Unavailable";
50
+ const safeAction = action || "startup";
51
+ const title = `[bug] ${safeAction} failed with exit ${exitCode}`;
52
+ const rawDiagnostics = [error?.stderr, error?.stdout].filter(Boolean).join("\n\n");
53
+ const redactedDiagnostics = rawDiagnostics
54
+ ? redact(rawDiagnostics, { cwd }).replaceAll("```", "` ` `")
55
+ : "";
56
+ const maxDiagnostics = 20_000;
57
+ const diagnostics = redactedDiagnostics.length > maxDiagnostics
58
+ ? `${redactedDiagnostics.slice(0, maxDiagnostics)}\n\n[diagnostics truncated]`
59
+ : redactedDiagnostics;
60
+ const body = `## Summary
61
+
62
+ AgentKit Helper failed while running \`${safeAction}\`.
63
+
64
+ ## Diagnostics
65
+
66
+ - Helper: ${helperVersion}
67
+ - Node: ${process.version}
68
+ - Platform: ${process.platform} ${process.arch}
69
+ - UI language: ${language}
70
+ - Exit code: ${exitCode}
71
+ - Error: ${redact(error?.message || "Unknown error", { cwd })}
72
+ - Command: \`${redact(command, { cwd })}\`
73
+
74
+ ${diagnostics ? `## Redacted ak diagnostics
75
+
76
+ \`\`\`text
77
+ ${diagnostics}
78
+ \`\`\`
79
+
80
+ ` : ""}## Privacy
81
+
82
+ Verbose ak stdout/stderr is included only after redacting home/project paths and common credential formats. Diagnostics are capped at ${maxDiagnostics} characters.
83
+ `;
84
+ return { title, body };
85
+ }
86
+
87
+ function runProcess(binary, args, { input = "" } = {}) {
88
+ return new Promise((resolve, reject) => {
89
+ const child = spawn(binary, args, {
90
+ shell: false,
91
+ stdio: ["pipe", "pipe", "pipe"],
92
+ env: process.env,
93
+ });
94
+ let stdout = "";
95
+ let stderr = "";
96
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
97
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
98
+ child.once("error", reject);
99
+ child.once("exit", (code, signal) => {
100
+ if (signal || code !== 0) {
101
+ const error = new Error(stderr.trim() || `${binary} exited with status ${code}`);
102
+ error.exitCode = code || 1;
103
+ reject(error);
104
+ return;
105
+ }
106
+ resolve(stdout.trim());
107
+ });
108
+ child.stdin.end(input);
109
+ });
110
+ }
111
+
112
+ export async function checkIssueRepository({
113
+ repository = resolveIssueRepository(), ghBinary = "gh", execute = runProcess,
114
+ } = {}) {
115
+ await execute(ghBinary, ["repo", "view", repository, "--json", "nameWithOwner"]);
116
+ return repository;
117
+ }
118
+
119
+ export async function findDuplicateIssue(report, {
120
+ repository = resolveIssueRepository(), ghBinary = "gh", execute = runProcess,
121
+ } = {}) {
122
+ const output = await execute(ghBinary, [
123
+ "issue", "list", "--repo", repository, "--state", "all",
124
+ "--search", `\"${report.title}\" in:title`, "--limit", "1", "--json", "title,url",
125
+ ]);
126
+ const issues = JSON.parse(output || "[]");
127
+ return issues.find((issue) => issue.title === report.title) || null;
128
+ }
129
+
130
+ export async function createGitHubIssue(report, {
131
+ repository = resolveIssueRepository(), ghBinary = "gh", execute = runProcess,
132
+ } = {}) {
133
+ return execute(ghBinary, [
134
+ "issue", "create", "--repo", repository,
135
+ "--title", report.title, "--body-file", "-",
136
+ ], { input: report.body });
137
+ }
package/lib/i18n.mjs ADDED
@@ -0,0 +1,255 @@
1
+ export const LANGUAGES = new Set(["vi", "en"]);
2
+
3
+ const messages = {
4
+ en: {
5
+ languagePrompt: "Choose interface language / Chọn ngôn ngữ giao diện",
6
+ commandPrompt: "What do you want to do?",
7
+ currentBinary: "Current ak binary: {version} ({channel})",
8
+ installAction: "Install a Kit",
9
+ updateAction: "Update a Kit",
10
+ selfUpdateAction: "Update ak binary",
11
+ updateAllAction: "Update all detected AgentKit installs",
12
+ exportAction: "Export Kit (not a runtime install)",
13
+ doctorAction: "Run health checks",
14
+ backToLanguage: "← Back to interface language",
15
+ chooseProject: "Choose a project directory",
16
+ currentProjectScope: "Use current project ({path})",
17
+ globalScope: "Use user/global Kit scope",
18
+ binaryScope: "Update only the ak binary",
19
+ scopePrompt: "Where should AgentKit manage this Kit?",
20
+ unsafeCwd: "The current directory ({cwd}) is not used as project scope.",
21
+ projectDirectory: "Project directory",
22
+ kitPrompt: "Which Kit do you want to use?",
23
+ engineerKit: "Engineer Kit",
24
+ marketingKit: "Marketing Kit",
25
+ targetPrompt: "Which coding runtimes should receive {kit}?",
26
+ updateTargetPrompt: "Which installed runtimes should update {kit}?",
27
+ allRuntimes: "All supported runtimes",
28
+ dshUpdateUnsupported: "dsh — not supported by ak update",
29
+ exportTargetPrompt: "Export creates a copy; it does not install a runtime. Choose a format:",
30
+ savedTargetNeedsUpdateRuntime: "Saved project target {target} is install-only. Update supports {supported}. Re-run with --runtime <target> or choose interactively.",
31
+ agyExport: "agy (global-only export)",
32
+ portableExport: "portable (output directory)",
33
+ exportDirectory: "Export output directory",
34
+ channelPrompt: "Which release channel?",
35
+ back: "← Back",
36
+ installPlan: "Install plan:",
37
+ betaKitStableBinaryWarning: "WARNING: Installed ak {version} appears to be stable. Installing a beta Kit with --yes may update the ak binary to beta first.",
38
+ betaKitLifecycleReason: "AgentKit enforces this lifecycle check to satisfy the beta Kit's required CLI version.",
39
+ binaryPreviewPrerequisite: "A signed {channel} binary update to {version} is required before all selected Kit previews can run.",
40
+ applyBinaryPrerequisite: "Update ak binary now, then continue previewing the selected Kits?",
41
+ binaryPrerequisitePlan: "Binary prerequisite:",
42
+ binaryPrerequisiteDryRun: "Dry run stops here because Kit previews require the newer binary. No changes were applied.",
43
+ binaryPrerequisiteComplete: "Binary prerequisite complete. Continuing Kit previews with ak {version}.",
44
+ binaryPrerequisiteDeclined: "Binary update declined; Kit previews were not run.",
45
+ dryRunFiles: "Dry run complete; no files were changed.",
46
+ runAk: "Run this official ak command?",
47
+ globalForceWarning: "WARNING: ak blocked the {target} global install because the target already exists or drift was detected. Global runtime files may contain user-installed or modified Skills. --force may replace or remove them; AgentKit creates recovery snapshots, but manual review may still be required.",
48
+ projectForceWarning: "WARNING: ak blocked the {target} project install because the target already exists or drift was detected. --force may replace or remove modified files.",
49
+ confirmForceInstall: "Force reinstall this target after the warning above?",
50
+ forceInstallPlan: "Explicit force reinstall plan:",
51
+ forceInstallDeclined: "Force reinstall declined; the existing target was not overwritten.",
52
+ forceNeedsConsent: "Force reinstall requires a separate interactive confirmation; --yes does not grant overwrite consent.",
53
+ runningAkCommand: "Running the official ak command with verbose diagnostics…",
54
+ akCommandComplete: "Official ak command completed.",
55
+ cancelledFiles: "Cancelled; no files were changed.",
56
+ savedChoice: "Saved project choice: {path}",
57
+ installComplete: "Install complete. Restart the selected runtime, then invoke a Skill from the selected Kit.",
58
+ projectRegistrationFailed: "Install succeeded, but this project could not be added to the ak registry: {message}",
59
+ binaryCheck: "Binary update check:",
60
+ binaryVersions: "Installed: {current} | {channel} latest: {latest}",
61
+ binaryAlreadyCurrent: "The ak binary is already current for this channel.",
62
+ binaryDowngradeWarning: "WARNING: {channel} {latest} is older than the installed {current}. ak self-update does not downgrade.",
63
+ binaryDowngradeRisk: "Downgrading may make newer AgentKit state incompatible with the older CLI.",
64
+ downgradePlan: "Explicit downgrade plan using the official signed installer:",
65
+ confirmDowngrade: "Are you sure you want to replace {current} with the older {latest}?",
66
+ downgradeNeedsConsent: "Downgrade requires an interactive confirmation, or both --allow-downgrade and --yes.",
67
+ cancelledDowngrade: "Downgrade cancelled; the binary was not changed.",
68
+ downgradeComplete: "Downgrade complete. Installed ak version: {version}",
69
+ binaryNoChange: "The official updater completed without changing the binary.",
70
+ binaryPlan: "Binary update plan:",
71
+ dryRunBinary: "Dry run complete; the binary was not changed.",
72
+ applyBinary: "Apply the signed binary update?",
73
+ cancelledBinary: "Cancelled; the binary was not changed.",
74
+ binaryComplete: "Binary update complete.",
75
+ loadingRegistry: "Loading projects from the ak registry…",
76
+ loadedRegistry: "Loaded registered AgentKit projects.",
77
+ deepScanning: "Scanning the selected folder for AgentKit projects…",
78
+ deepScanComplete: "Project scan complete.",
79
+ customDeepScanAction: "Choose a project manually…",
80
+ deepScanResult: "Found {count} additional updateable project(s) in the selected folder.",
81
+ deepScanRootsPrompt: "Choose a parent directory to scan (type to filter, use arrows and Enter; not / or your whole home)",
82
+ inventory: "Detected update inventory:",
83
+ registeredProjects: "Projects from ak projects list:",
84
+ unregisteredProjects: "Other discovered projects:",
85
+ otherInstalls: "Global scope:",
86
+ globalCandidate: "{kit} global scope ({runtimes})",
87
+ projectCandidate: "{name} — {kit} ({runtime}) — {path}",
88
+ selectUpdateAll: "Select what to update (Space toggles, Enter confirms)",
89
+ updateEverything: "Update all discovered Kit installs: global + projects ({count})",
90
+ updateAllProjects: "Update projects only ({count})",
91
+ chooseUpdates: "Choose individual targets…",
92
+ backFromTargetSelection: "← Back (select with Space, then confirm with Enter)",
93
+ confirmSelectedUpdates: "What should happen with the {count} selected target(s)?",
94
+ previewSelectedUpdates: "Continue to preview ({count})",
95
+ noProjectsInRegistry: "No updateable registered projects were found.",
96
+ updateAllBack: "← Back to release channel",
97
+ discoveryWarning: "Discovery warning: {message}",
98
+ noUpdateCandidates: "No updateable Engineer or Marketing Kit installs were detected.",
99
+ updateAllPreview: "Previewing selected updates:",
100
+ updateAllApplyPlan: "Update-all apply plan:",
101
+ applyAll: "Apply every selected update sequentially?",
102
+ updateProgress: "[{current}/{total}] Updating {label}",
103
+ cancelledAll: "Cancelled; no selected update was applied.",
104
+ updateAllComplete: "Selected updates completed. Restart the affected runtimes.",
105
+ updatePreview: "Kit update preview:",
106
+ globalUpdateSafety: "Global update is preserve-only: ak skips user-modified files, and the helper never adds --force.",
107
+ applyPlan: "Apply plan:",
108
+ applyKit: "Apply this Kit update?",
109
+ cancelledUpdate: "Cancelled; no update was applied.",
110
+ updateComplete: "Update complete. Restart the selected runtime and verify a Skill from the selected Kit.",
111
+ exportPlan: "Export plan:",
112
+ runExport: "Run this official ak export command?",
113
+ exportComplete: "Export complete.",
114
+ done: "Done",
115
+ promptCancelled: "Operation cancelled; no helper action was applied.",
116
+ error: "ERROR: {message}",
117
+ issuePreview: "A redacted GitHub issue report is ready:",
118
+ createIssue: "Create this issue in {repo}?",
119
+ issueSkipped: "GitHub issue was not created.",
120
+ issueDuplicate: "A matching issue already exists: {url}",
121
+ issueCreated: "GitHub issue created: {url}",
122
+ issueFailed: "Could not create the GitHub issue: {message}",
123
+ issueManual: "Report it manually: https://github.com/thieung/agentkit-helper/issues/new",
124
+ issueRepoUnavailable: "GitHub issue reporting is unavailable because repository {repo} cannot be accessed.",
125
+ issueRepoSetup: "Publish that repository first, or set AK_HELPER_ISSUE_REPO=owner/repository.",
126
+ },
127
+ vi: {
128
+ languagePrompt: "Chọn ngôn ngữ giao diện / Choose interface language",
129
+ commandPrompt: "Bạn muốn làm gì?",
130
+ currentBinary: "ak binary hiện tại: {version} ({channel})",
131
+ installAction: "Cài Kit",
132
+ updateAction: "Cập nhật Kit",
133
+ selfUpdateAction: "Cập nhật ak binary",
134
+ updateAllAction: "Cập nhật mọi AgentKit install đã detect",
135
+ exportAction: "Export Kit (không cài vào runtime)",
136
+ doctorAction: "Chạy health check",
137
+ backToLanguage: "← Quay lại chọn ngôn ngữ",
138
+ chooseProject: "Chọn thư mục project",
139
+ currentProjectScope: "Dùng project hiện tại ({path})",
140
+ globalScope: "Dùng Kit ở scope user/global",
141
+ binaryScope: "Chỉ cập nhật binary ak",
142
+ scopePrompt: "AgentKit sẽ quản lý Kit này ở scope nào?",
143
+ unsafeCwd: "Thư mục hiện tại ({cwd}) không được dùng làm project scope.",
144
+ projectDirectory: "Thư mục project",
145
+ kitPrompt: "Bạn muốn dùng Kit nào?",
146
+ engineerKit: "Engineer Kit",
147
+ marketingKit: "Marketing Kit",
148
+ targetPrompt: "Các runtime nào sẽ nhận {kit}?",
149
+ updateTargetPrompt: "Các runtime nào sẽ cập nhật {kit}?",
150
+ allRuntimes: "Tất cả runtime được hỗ trợ",
151
+ dshUpdateUnsupported: "dsh — ak update chưa hỗ trợ",
152
+ exportTargetPrompt: "Export tạo một bản sao, không cài vào runtime. Chọn dạng export:",
153
+ savedTargetNeedsUpdateRuntime: "Target đã lưu của project là {target} và chỉ dùng cho install. Update chỉ hỗ trợ {supported}. Hãy chạy lại với --runtime <target> hoặc chọn lại trong chế độ interactive.",
154
+ agyExport: "agy (chỉ export global)",
155
+ portableExport: "portable (chọn thư mục output)",
156
+ exportDirectory: "Thư mục output cho export",
157
+ channelPrompt: "Chọn release channel?",
158
+ back: "← Quay lại",
159
+ installPlan: "Kế hoạch cài đặt:",
160
+ betaKitStableBinaryWarning: "WARNING: ak {version} đang cài có vẻ là stable. Cài Kit beta với --yes có thể cập nhật ak binary sang beta trước.",
161
+ betaKitLifecycleReason: "AgentKit enforce lifecycle check này để đáp ứng CLI version mà Kit beta yêu cầu.",
162
+ binaryPreviewPrerequisite: "Cần cập nhật signed binary channel {channel} lên {version} trước khi preview đầy đủ các Kit đã chọn.",
163
+ applyBinaryPrerequisite: "Cập nhật ak binary ngay rồi tiếp tục preview các Kit đã chọn?",
164
+ binaryPrerequisitePlan: "Binary prerequisite:",
165
+ binaryPrerequisiteDryRun: "Dry run dừng tại đây vì Kit preview cần binary mới hơn. Chưa có thay đổi nào được áp dụng.",
166
+ binaryPrerequisiteComplete: "Đã cập nhật binary prerequisite. Tiếp tục preview Kit bằng ak {version}.",
167
+ binaryPrerequisiteDeclined: "Đã từ chối cập nhật binary; chưa chạy Kit preview.",
168
+ dryRunFiles: "Dry run hoàn tất; không có file nào bị thay đổi.",
169
+ runAk: "Chạy command ak chính thức này?",
170
+ globalForceWarning: "WARNING: ak đã chặn cài global cho {target} vì target đã tồn tại hoặc phát hiện drift. Global runtime có thể chứa Skill do user cài hoặc chỉnh sửa. --force có thể thay thế hoặc xóa các file này; AgentKit có tạo recovery snapshot nhưng vẫn có thể cần kiểm tra thủ công.",
171
+ projectForceWarning: "WARNING: ak đã chặn cài project cho {target} vì target đã tồn tại hoặc phát hiện drift. --force có thể thay thế hoặc xóa file đã chỉnh sửa.",
172
+ confirmForceInstall: "Vẫn force reinstall target này sau cảnh báo trên?",
173
+ forceInstallPlan: "Kế hoạch force reinstall đã được xác nhận:",
174
+ forceInstallDeclined: "Đã từ chối force reinstall; target hiện tại không bị ghi đè.",
175
+ forceNeedsConsent: "Force reinstall cần một xác nhận interactive riêng; --yes không đồng nghĩa cho phép ghi đè.",
176
+ runningAkCommand: "Đang chạy command ak chính thức với verbose diagnostics…",
177
+ akCommandComplete: "Command ak chính thức đã hoàn tất.",
178
+ cancelledFiles: "Đã hủy; không có file nào bị thay đổi.",
179
+ savedChoice: "Đã lưu lựa chọn project: {path}",
180
+ installComplete: "Cài đặt hoàn tất. Hãy khởi động lại runtime rồi gọi một Skill từ Kit đã chọn.",
181
+ projectRegistrationFailed: "Cài đặt thành công, nhưng chưa thể thêm project này vào ak registry: {message}",
182
+ binaryCheck: "Kiểm tra cập nhật binary:",
183
+ binaryVersions: "Đang cài: {current} | {channel} mới nhất: {latest}",
184
+ binaryAlreadyCurrent: "ak binary đã là bản hiện tại của channel này.",
185
+ binaryDowngradeWarning: "WARNING: {channel} {latest} thấp hơn bản đang cài {current}. ak self-update không downgrade.",
186
+ binaryDowngradeRisk: "Downgrade có thể khiến AgentKit state mới không tương thích với CLI cũ hơn.",
187
+ downgradePlan: "Kế hoạch downgrade tường minh bằng official signed installer:",
188
+ confirmDowngrade: "Bạn có chắc muốn thay {current} bằng bản thấp hơn {latest}?",
189
+ downgradeNeedsConsent: "Downgrade cần xác nhận interactive, hoặc dùng đồng thời --allow-downgrade và --yes.",
190
+ cancelledDowngrade: "Đã hủy downgrade; binary không bị thay đổi.",
191
+ downgradeComplete: "Downgrade hoàn tất. Phiên bản ak đang cài: {version}",
192
+ binaryNoChange: "Official updater hoàn tất nhưng không thay đổi binary.",
193
+ binaryPlan: "Kế hoạch cập nhật binary:",
194
+ dryRunBinary: "Dry run hoàn tất; binary không bị thay đổi.",
195
+ applyBinary: "Áp dụng bản cập nhật binary đã ký?",
196
+ cancelledBinary: "Đã hủy; binary không bị thay đổi.",
197
+ binaryComplete: "Cập nhật binary hoàn tất.",
198
+ loadingRegistry: "Đang tải project từ ak registry…",
199
+ loadedRegistry: "Đã tải các AgentKit project được đăng ký.",
200
+ deepScanning: "Đang tìm AgentKit project trong thư mục đã chọn…",
201
+ deepScanComplete: "Đã tìm project xong.",
202
+ customDeepScanAction: "Chọn project thủ công…",
203
+ deepScanResult: "Tìm thấy thêm {count} project có thể update trong thư mục đã chọn.",
204
+ deepScanRootsPrompt: "Chọn thư mục cha cần scan (gõ để lọc, dùng ↑/↓ và Enter; không dùng / hoặc toàn bộ home)",
205
+ inventory: "Danh sách update đã detect:",
206
+ registeredProjects: "Project từ ak projects list:",
207
+ unregisteredProjects: "Project khác đã discover:",
208
+ otherInstalls: "Global scope:",
209
+ globalCandidate: "{kit} global scope ({runtimes})",
210
+ projectCandidate: "{name} — {kit} ({runtime}) — {path}",
211
+ selectUpdateAll: "Chọn mục cần update (Space bật/tắt, Enter xác nhận)",
212
+ updateEverything: "Update mọi Kit đã detect: global + project ({count})",
213
+ updateAllProjects: "Chỉ update các project đã discover ({count})",
214
+ chooseUpdates: "Chọn từng target…",
215
+ backFromTargetSelection: "← Quay lại (nhấn Space rồi Enter)",
216
+ confirmSelectedUpdates: "Xử lý thế nào với {count} target đã chọn?",
217
+ previewSelectedUpdates: "Tiếp tục preview ({count})",
218
+ noProjectsInRegistry: "Không tìm thấy registered project nào có thể update.",
219
+ updateAllBack: "← Quay lại chọn release channel",
220
+ discoveryWarning: "Cảnh báo discovery: {message}",
221
+ noUpdateCandidates: "Không detect được Engineer hoặc Marketing Kit install nào có thể update.",
222
+ updateAllPreview: "Đang preview các update đã chọn:",
223
+ updateAllApplyPlan: "Kế hoạch Update all:",
224
+ applyAll: "Áp dụng tuần tự mọi update đã chọn?",
225
+ updateProgress: "[{current}/{total}] Đang update {label}",
226
+ cancelledAll: "Đã hủy; chưa áp dụng update nào đã chọn.",
227
+ updateAllComplete: "Đã update xong các mục được chọn. Hãy khởi động lại runtime liên quan.",
228
+ updatePreview: "Xem trước cập nhật Kit:",
229
+ globalUpdateSafety: "Global update dùng preserve-only: ak bỏ qua file do user chỉnh sửa và helper không bao giờ thêm --force.",
230
+ applyPlan: "Kế hoạch áp dụng:",
231
+ applyKit: "Áp dụng bản cập nhật Kit này?",
232
+ cancelledUpdate: "Đã hủy; chưa áp dụng cập nhật.",
233
+ updateComplete: "Cập nhật hoàn tất. Hãy khởi động lại runtime và kiểm tra một Skill từ Kit đã chọn.",
234
+ exportPlan: "Kế hoạch export:",
235
+ runExport: "Chạy command export ak chính thức này?",
236
+ exportComplete: "Export hoàn tất.",
237
+ done: "Hoàn tất",
238
+ promptCancelled: "Đã hủy thao tác; helper chưa áp dụng thay đổi nào.",
239
+ error: "LỖI: {message}",
240
+ issuePreview: "Report GitHub issue đã được lọc thông tin nhạy cảm:",
241
+ createIssue: "Tạo issue này trong {repo}?",
242
+ issueSkipped: "Chưa tạo GitHub issue.",
243
+ issueDuplicate: "Đã có issue tương tự: {url}",
244
+ issueCreated: "Đã tạo GitHub issue: {url}",
245
+ issueFailed: "Không thể tạo GitHub issue: {message}",
246
+ issueManual: "Báo lỗi thủ công tại: https://github.com/thieung/agentkit-helper/issues/new",
247
+ issueRepoUnavailable: "Không thể report GitHub issue vì repository {repo} không truy cập được.",
248
+ issueRepoSetup: "Hãy publish repository đó trước, hoặc đặt AK_HELPER_ISSUE_REPO=owner/repository.",
249
+ },
250
+ };
251
+
252
+ export function t(language, key, values = {}) {
253
+ const template = messages[language]?.[key] ?? messages.en[key] ?? key;
254
+ return template.replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
255
+ }
@@ -0,0 +1,22 @@
1
+ export const BACK = Symbol("back");
2
+
3
+ export async function walkSelections(steps) {
4
+ const values = {};
5
+ let index = 0;
6
+
7
+ while (index < steps.length) {
8
+ const step = steps[index];
9
+ const value = await step.select(values);
10
+ if (value === BACK) {
11
+ delete values[step.key];
12
+ if (index === 0) return BACK;
13
+ index -= 1;
14
+ delete values[steps[index].key];
15
+ continue;
16
+ }
17
+ values[step.key] = value;
18
+ index += 1;
19
+ }
20
+
21
+ return values;
22
+ }
@@ -0,0 +1,37 @@
1
+ import { homedir } from "node:os";
2
+ import { parse, resolve } from "node:path";
3
+ import { realpath, stat } from "node:fs/promises";
4
+
5
+ export function isUnsafeProjectPath(path, home = homedir()) {
6
+ const absolute = resolve(path);
7
+ return absolute === parse(absolute).root || absolute === resolve(home);
8
+ }
9
+
10
+ export async function resolveProjectPath(input, cwd = process.cwd(), home = homedir()) {
11
+ const project = resolve(input || cwd);
12
+ if (isUnsafeProjectPath(project, home)) {
13
+ throw new Error(
14
+ `not using ${project} as project scope; choose --project <path>, --global, or update --binary-only`,
15
+ );
16
+ }
17
+
18
+ let metadata;
19
+ try {
20
+ metadata = await stat(project);
21
+ } catch (error) {
22
+ if (error.code === "ENOENT") {
23
+ throw new Error(`project directory does not exist: ${project}`);
24
+ }
25
+ throw error;
26
+ }
27
+ if (!metadata.isDirectory()) {
28
+ throw new Error(`project path is not a directory: ${project}`);
29
+ }
30
+ const canonicalProject = await realpath(project);
31
+ if (isUnsafeProjectPath(canonicalProject, home)) {
32
+ throw new Error(
33
+ `not using ${project} as project scope because it resolves to ${canonicalProject}`,
34
+ );
35
+ }
36
+ return canonicalProject;
37
+ }
@@ -0,0 +1,149 @@
1
+ import {
2
+ cancel,
3
+ confirm as clackConfirm,
4
+ intro,
5
+ isCancel,
6
+ multiselect,
7
+ outro,
8
+ path as clackPath,
9
+ select,
10
+ spinner,
11
+ text,
12
+ } from "@clack/prompts";
13
+ import { stdin, stdout } from "node:process";
14
+ import { colorText } from "./colors.mjs";
15
+ import { BACK } from "./navigation.mjs";
16
+
17
+ let sessionStarted = false;
18
+ let promptCopy = {
19
+ intro: "AgentKit Helper",
20
+ cancelled: "Operation cancelled; no helper action was applied.",
21
+ };
22
+
23
+ export class PromptCancelledError extends Error {
24
+ constructor() {
25
+ super("interactive session cancelled");
26
+ this.name = "PromptCancelledError";
27
+ }
28
+ }
29
+
30
+ function requireTty() {
31
+ if (!stdin.isTTY || !stdout.isTTY) {
32
+ throw new Error("interactive input is unavailable; pass explicit flags and --yes");
33
+ }
34
+ }
35
+
36
+ function startSession() {
37
+ requireTty();
38
+ if (!sessionStarted) {
39
+ intro(promptCopy.intro);
40
+ sessionStarted = true;
41
+ }
42
+ }
43
+
44
+ function unwrap(value) {
45
+ if (!isCancel(value)) return value;
46
+ cancel(promptCopy.cancelled);
47
+ throw new PromptCancelledError();
48
+ }
49
+
50
+ function highlightPrompt(message) {
51
+ return colorText(message, "prompt");
52
+ }
53
+
54
+ export async function choose(message, choices, defaultIndex = 0) {
55
+ startSession();
56
+ return unwrap(await select({
57
+ message: highlightPrompt(message),
58
+ options: choices,
59
+ initialValue: choices[defaultIndex]?.value,
60
+ }));
61
+ }
62
+
63
+ export async function chooseWithBack(message, choices, defaultIndex = 0, backLabel = "← Back") {
64
+ return choose(message, [
65
+ ...choices,
66
+ { label: backLabel, value: BACK },
67
+ ], defaultIndex);
68
+ }
69
+
70
+ export async function multiChoose(message, choices, initialValues = choices.map((choice) => choice.value)) {
71
+ startSession();
72
+ return unwrap(await multiselect({
73
+ message: highlightPrompt(message),
74
+ options: choices,
75
+ initialValues,
76
+ required: true,
77
+ }));
78
+ }
79
+
80
+ export async function multiChooseWithBack(
81
+ message,
82
+ choices,
83
+ initialValues = choices.map((choice) => choice.value),
84
+ backLabel = "← Back (select with Space, then confirm with Enter)",
85
+ ) {
86
+ const values = await multiChoose(message, [
87
+ ...choices,
88
+ { label: backLabel, value: BACK },
89
+ ], initialValues);
90
+ return values.includes(BACK) ? BACK : values;
91
+ }
92
+
93
+ export async function ask(message) {
94
+ startSession();
95
+ const value = unwrap(await text({
96
+ message: highlightPrompt(message),
97
+ validate(input) {
98
+ if (!input.trim()) return `${message} is required`;
99
+ },
100
+ }));
101
+ return value.trim();
102
+ }
103
+
104
+ export async function askDirectory(message, { root = process.cwd() } = {}) {
105
+ startSession();
106
+ return unwrap(await clackPath({
107
+ message: highlightPrompt(message),
108
+ root,
109
+ initialValue: root,
110
+ directory: true,
111
+ }));
112
+ }
113
+
114
+ export function confirmPromptOptions(message, initialValue = true) {
115
+ return { message: highlightPrompt(message), initialValue };
116
+ }
117
+
118
+ export async function confirm(message, initialValue = true) {
119
+ startSession();
120
+ return unwrap(await clackConfirm(confirmPromptOptions(message, initialValue)));
121
+ }
122
+
123
+ export async function withSpinner(message, completeMessage, task) {
124
+ if (!stdin.isTTY || !stdout.isTTY) return task();
125
+ startSession();
126
+ const progress = spinner();
127
+ progress.start(highlightPrompt(message));
128
+ try {
129
+ const result = await task();
130
+ progress.stop(highlightPrompt(completeMessage));
131
+ return result;
132
+ } catch (error) {
133
+ progress.error(highlightPrompt(message));
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ export function warning(message) {
139
+ const output = colorText(message, "warning", { stream: process.stderr });
140
+ process.stderr.write(`${output}\n`);
141
+ }
142
+
143
+ export function setPromptCopy(copy) {
144
+ promptCopy = { ...promptCopy, ...copy };
145
+ }
146
+
147
+ export function finishInteractive(message) {
148
+ if (sessionStarted) outro(message);
149
+ }