@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,1109 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, realpath } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import {
8
+ EXPORT_TARGETS,
9
+ INSTALL_TARGETS,
10
+ KITS,
11
+ parseArgs,
12
+ splitTargetSpec,
13
+ targetSpecIsSupported,
14
+ UPDATE_TARGETS,
15
+ validateForCommand,
16
+ } from "../lib/args.mjs";
17
+ import {
18
+ exportArgs,
19
+ formatCommand,
20
+ globalUpdateApplyArgs,
21
+ globalUpdatePreviewArgs,
22
+ installArgs,
23
+ projectUpdateApplyArgs,
24
+ projectUpdatePreviewArgs,
25
+ selfUpdateCheckArgs,
26
+ selfUpdateApplyArgs,
27
+ selfUpdateJsonApplyArgs,
28
+ selfUpdateJsonCheckArgs,
29
+ updateApplyArgs,
30
+ updatePreviewArgs,
31
+ } from "../lib/commands.mjs";
32
+ import { readProjectConfig, writeProjectConfig } from "../lib/config.mjs";
33
+ import { colorText } from "../lib/colors.mjs";
34
+ import {
35
+ discoverGlobalKitInstalls,
36
+ discoverProjectCandidates,
37
+ } from "../lib/discovery.mjs";
38
+ import {
39
+ buildIssueReport,
40
+ checkIssueRepository,
41
+ createGitHubIssue,
42
+ findDuplicateIssue,
43
+ isReportableAkError,
44
+ resolveIssueRepository,
45
+ } from "../lib/github-issue.mjs";
46
+ import { t } from "../lib/i18n.mjs";
47
+ import { isUnsafeProjectPath, resolveProjectPath } from "../lib/project.mjs";
48
+ import { BACK, walkSelections } from "../lib/navigation.mjs";
49
+ import {
50
+ ask,
51
+ askDirectory,
52
+ choose,
53
+ chooseWithBack,
54
+ confirm,
55
+ finishInteractive,
56
+ multiChoose,
57
+ multiChooseWithBack,
58
+ PromptCancelledError,
59
+ setPromptCopy,
60
+ warning,
61
+ withSpinner,
62
+ } from "../lib/prompts.mjs";
63
+ import {
64
+ ensureAk,
65
+ requiresForceConsent,
66
+ run,
67
+ runCapture,
68
+ runPipeline,
69
+ } from "../lib/runner.mjs";
70
+ import {
71
+ assertInstallerVersion,
72
+ classifySelfUpdate,
73
+ parseSelfUpdateOutput,
74
+ releaseChannelForVersion,
75
+ } from "../lib/self-update.mjs";
76
+
77
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
78
+ const metadata = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
79
+ const akBinary = process.env.AK_HELPER_AK_BIN || "ak";
80
+ const ghBinary = process.env.AK_HELPER_GH_BIN || "gh";
81
+ const installerUrl = "https://agentkit.best/install.sh";
82
+ const windowsInstallerUrl = "https://agentkit.best/install.ps1";
83
+ let activeLanguage = "en";
84
+ let activeAction = null;
85
+ let installedAkVersion = "";
86
+
87
+ function ui(key, values) {
88
+ return t(activeLanguage, key, values);
89
+ }
90
+
91
+ function showCurrentBinary() {
92
+ const version = installedAkVersion.replace(/^ak\s+/i, "");
93
+ process.stdout.write(`${colorText(`│ ${ui("currentBinary", {
94
+ version,
95
+ channel: releaseChannelForVersion(installedAkVersion) || "stable",
96
+ })}`, "binary")}\n`);
97
+ }
98
+
99
+ function usage(language = "en") {
100
+ if (language === "vi") {
101
+ process.stdout.write(`AgentKit Helper ${metadata.version}
102
+
103
+ Cách dùng:
104
+ agentkit-helper install [tùy chọn]
105
+ agentkit-helper update [tùy chọn]
106
+ agentkit-helper self-update [tùy chọn]
107
+ agentkit-helper update-all [tùy chọn]
108
+ agentkit-helper export [tùy chọn]
109
+ agentkit-helper doctor [--project <đường-dẫn>]
110
+
111
+ Tùy chọn:
112
+ --project <đường-dẫn> Dùng project scope tại đường dẫn này
113
+ --global Dùng scope user/global của runtime
114
+ --target <targets> Các runtime phân cách dấu phẩy cho install/update, hoặc một export target
115
+ --runtime <runtimes> Alias của --target nhưng chỉ cho runtime install/update
116
+ --kit <kit> engineer hoặc marketing
117
+ --channel <channel> stable hoặc beta
118
+ --language <vi|en> Ngôn ngữ giao diện helper
119
+ --out <đường-dẫn> Thư mục output cho portable export
120
+ --binary-only Chỉ cập nhật binary ak
121
+ --allow-downgrade Cho phép downgrade khi dùng cùng --yes
122
+ --deep-scan <thư-mục> Deep scan thư mục cha (có thể lặp lại; chỉ update-all)
123
+ --max-depth <1-20> Độ sâu deep scan, mặc định 5
124
+ --exclude <tên,...> Bỏ qua thêm các tên thư mục khi deep scan
125
+ --dry-run Lập kế hoạch/xem trước, không thay đổi
126
+ --yes, -y Bỏ qua bước xác nhận của helper
127
+ --no-save Không ghi .ak-kit.json
128
+ --help, -h Hiện trợ giúp
129
+ --version, -v Hiện phiên bản
130
+
131
+ Target groups:
132
+ Install claude-code, codex, cursor, dsh, grok, omp, pi
133
+ Update claude-code, codex, cursor, grok, omp, pi (dsh: unsupported)
134
+ Export agy, portable
135
+ `);
136
+ return;
137
+ }
138
+ process.stdout.write(`AgentKit Helper ${metadata.version}
139
+
140
+ Usage:
141
+ agentkit-helper install [options]
142
+ agentkit-helper update [options]
143
+ agentkit-helper self-update [options]
144
+ agentkit-helper update-all [options]
145
+ agentkit-helper export [options]
146
+ agentkit-helper doctor [--project <path>]
147
+
148
+ Options:
149
+ --project <path> Use project scope at this path
150
+ --global Use the runtime user/global scope
151
+ --target <targets> Comma-separated runtimes for install/update, or one export target
152
+ --runtime <runtimes> Alias of --target for install/update runtimes only
153
+ --kit <kit> engineer or marketing
154
+ --channel <channel> stable or beta
155
+ --language <vi|en> Helper interface language
156
+ --out <path> Output directory for portable export
157
+ --binary-only Update only the ak binary
158
+ --allow-downgrade Permit downgrade when also used with --yes
159
+ --deep-scan <path> Deep scan a parent path (repeatable; update-all only)
160
+ --max-depth <1-20> Deep scan depth, default 5
161
+ --exclude <name,...> Additional directory names to skip during deep scan
162
+ --dry-run Plan or preview without mutation
163
+ --yes, -y Skip helper confirmation
164
+ --no-save Do not write .ak-kit.json
165
+ --help, -h Show help
166
+ --version, -v Show version
167
+
168
+ Target groups:
169
+ Install claude-code, codex, cursor, dsh, grok, omp, pi
170
+ Update claude-code, codex, cursor, grok, omp, pi (dsh: unsupported)
171
+ Export agy, portable
172
+ `);
173
+ }
174
+
175
+ function printPlan(args, cwd) {
176
+ const suffix = cwd && cwd !== process.cwd() ? ` (cwd: ${cwd})` : "";
177
+ process.stdout.write(`${colorText(` ${formatCommand(akBinary, args)}${suffix}`, "command")}\n`);
178
+ }
179
+
180
+ function printSection(message) {
181
+ process.stdout.write(`\n${colorText(message, "section")}\n`);
182
+ }
183
+
184
+ async function runAkCommand(args, { cwd = process.cwd() } = {}) {
185
+ const result = await withSpinner(
186
+ ui("runningAkCommand"),
187
+ ui("akCommandComplete"),
188
+ () => runCapture(akBinary, args, { cwd }),
189
+ );
190
+ if (result.stdout) process.stdout.write(`${result.stdout}\n`);
191
+ if (result.stderr) process.stderr.write(`${result.stderr}\n`);
192
+ }
193
+
194
+ async function chooseCommand(allowLanguageBack = false) {
195
+ const choices = [
196
+ { label: ui("installAction"), value: "install" },
197
+ { label: ui("updateAction"), value: "update" },
198
+ { label: ui("selfUpdateAction"), value: "self-update" },
199
+ { label: ui("updateAllAction"), value: "update-all" },
200
+ { label: ui("exportAction"), value: "export" },
201
+ { label: ui("doctorAction"), value: "doctor" },
202
+ ];
203
+ if (allowLanguageBack) {
204
+ choices.push({ label: ui("backToLanguage"), value: BACK });
205
+ }
206
+ return choose(ui("commandPrompt"), choices);
207
+ }
208
+
209
+ function kitName(kit) {
210
+ return ui(kit === "marketing" ? "marketingKit" : "engineerKit");
211
+ }
212
+
213
+ async function selectKit(options, config, allowBack = false, forcePrompt = false) {
214
+ if (options.kit) return options.kit;
215
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return config?.kit || "engineer";
216
+ if (config?.kit && !forcePrompt) return config.kit;
217
+ return chooseLocalized(allowBack, ui("kitPrompt"), [...KITS].map((kit) => ({
218
+ label: kitName(kit),
219
+ value: kit,
220
+ })));
221
+ }
222
+
223
+ async function selectLanguage(options, forcePrompt = false) {
224
+ if (options.language && !forcePrompt) return options.language;
225
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return "en";
226
+ return choose(t("en", "languagePrompt"), [
227
+ { label: "Tiếng Việt", value: "vi" },
228
+ { label: "English", value: "en" },
229
+ ]);
230
+ }
231
+
232
+ function chooseLocalized(allowBack, message, choices, defaultIndex = 0) {
233
+ return allowBack
234
+ ? chooseWithBack(message, choices, defaultIndex, ui("back"))
235
+ : choose(message, choices, defaultIndex);
236
+ }
237
+
238
+ function supportedList(targets) {
239
+ return [...targets].join(", ");
240
+ }
241
+
242
+ function needsScopePrompt(options) {
243
+ return !options.binaryOnly && !options.global && !options.project && (
244
+ (process.stdin.isTTY && process.stdout.isTTY) || isUnsafeProjectPath(process.cwd())
245
+ );
246
+ }
247
+
248
+ async function selectScope(command, options, allowBack = false) {
249
+ if (options.binaryOnly) {
250
+ return { binaryOnly: true, global: false, project: null };
251
+ }
252
+ if (options.global) {
253
+ return { binaryOnly: false, global: true, project: null };
254
+ }
255
+ if (options.project) {
256
+ return {
257
+ binaryOnly: false,
258
+ global: false,
259
+ project: await resolveProjectPath(options.project),
260
+ };
261
+ }
262
+ const currentProjectIsSafe = !isUnsafeProjectPath(process.cwd());
263
+ if (currentProjectIsSafe && (!process.stdin.isTTY || !process.stdout.isTTY)) {
264
+ return {
265
+ binaryOnly: false,
266
+ global: false,
267
+ project: await resolveProjectPath(null),
268
+ };
269
+ }
270
+
271
+ const choices = [
272
+ ...(currentProjectIsSafe ? [{
273
+ label: ui("currentProjectScope", { path: process.cwd() }),
274
+ value: "current-project",
275
+ }] : []),
276
+ { label: ui("chooseProject"), value: "project" },
277
+ { label: ui("globalScope"), value: "global" },
278
+ ];
279
+ if (command === "update") {
280
+ choices.unshift({ label: ui("binaryScope"), value: "binary" });
281
+ }
282
+ const scope = await chooseLocalized(
283
+ allowBack,
284
+ currentProjectIsSafe ? ui("scopePrompt") : ui("unsafeCwd", { cwd: process.cwd() }),
285
+ choices,
286
+ );
287
+ if (scope === BACK) return BACK;
288
+ if (scope === "current-project") {
289
+ return {
290
+ binaryOnly: false,
291
+ global: false,
292
+ project: await resolveProjectPath(null),
293
+ };
294
+ }
295
+ if (scope === "binary") {
296
+ return { binaryOnly: true, global: false, project: null };
297
+ }
298
+ if (scope === "global") {
299
+ return { binaryOnly: false, global: true, project: null };
300
+ }
301
+ return {
302
+ binaryOnly: false,
303
+ global: false,
304
+ project: await resolveProjectPath(await askDirectory(ui("projectDirectory"), { root: homedir() })),
305
+ };
306
+ }
307
+
308
+ async function selectTarget(
309
+ options,
310
+ config,
311
+ targets,
312
+ promptKey,
313
+ allowBack = false,
314
+ forcePrompt = false,
315
+ promptValues = {},
316
+ ) {
317
+ if (options.target) return options.target;
318
+ if (options.runtime) return options.runtime;
319
+ if (config?.target && targetSpecIsSupported(config.target, targets) && !forcePrompt) {
320
+ return config.target;
321
+ }
322
+ const allRuntimeValue = "__all_runtimes__";
323
+ const choices = [
324
+ { label: ui("allRuntimes"), value: allRuntimeValue },
325
+ ...[...targets].map((target) => ({ label: target, value: target })),
326
+ ];
327
+ if (promptKey === "updateTargetPrompt") {
328
+ choices.push({
329
+ label: ui("dshUpdateUnsupported"),
330
+ value: "dsh",
331
+ disabled: true,
332
+ });
333
+ }
334
+ const configuredTargets = splitTargetSpec(config?.target).filter((target) => targets.has(target));
335
+ const initialValues = configuredTargets.length > 0 ? configuredTargets : ["codex"];
336
+ const selected = await (allowBack ? multiChooseWithBack : multiChoose)(
337
+ ui(promptKey, promptValues),
338
+ choices,
339
+ initialValues,
340
+ ...(allowBack ? [ui("backFromTargetSelection")] : []),
341
+ );
342
+ if (selected === BACK) return BACK;
343
+ if (selected.includes(allRuntimeValue)) return [...targets].join(",");
344
+ return [...targets].filter((target) => selected.includes(target)).join(",");
345
+ }
346
+
347
+ async function selectChannel(options, config, allowBack = false, forcePrompt = false) {
348
+ if (options.channel) return options.channel;
349
+ if (config?.channel && !forcePrompt) return config.channel;
350
+ const choices = [
351
+ { label: "stable", value: "stable" },
352
+ { label: "beta", value: "beta" },
353
+ ];
354
+ const defaultChannel = releaseChannelForVersion(installedAkVersion) || config?.channel || "stable";
355
+ const defaultIndex = defaultChannel === "beta" ? 1 : 0;
356
+ return chooseLocalized(allowBack, ui("channelPrompt"), choices, defaultIndex);
357
+ }
358
+
359
+ async function approve(options, message) {
360
+ if (options.yes) return true;
361
+ return confirm(message);
362
+ }
363
+
364
+ function warnBetaLifecycle(channel) {
365
+ if (channel !== "beta") return;
366
+ const version = installedAkVersion.replace(/^ak\s+/i, "");
367
+ if (!version || version.includes("-")) return;
368
+ warning(`${ui("betaKitStableBinaryWarning", { version })}\n${ui("betaKitLifecycleReason")}`);
369
+ }
370
+
371
+ async function prepareBetaBinary(commandOptions, { requiresPreview = false } = {}) {
372
+ const currentVersion = installedAkVersion.replace(/^ak\s+/i, "");
373
+ if (!currentVersion || currentVersion.includes("-")) {
374
+ return { proceed: true, updated: false };
375
+ }
376
+ warnBetaLifecycle("beta");
377
+ const check = parseSelfUpdateOutput((await runCapture(
378
+ akBinary, selfUpdateJsonCheckArgs("beta"),
379
+ )).stdout);
380
+ if (classifySelfUpdate(check) !== "update") {
381
+ return { proceed: true, updated: false };
382
+ }
383
+ warning(ui("binaryPreviewPrerequisite", {
384
+ channel: "beta",
385
+ version: check.latest_version,
386
+ }));
387
+ printSection(ui("binaryPrerequisitePlan"));
388
+ printPlan(selfUpdateApplyArgs("beta"));
389
+ if (commandOptions.dryRun) {
390
+ if (requiresPreview) {
391
+ process.stdout.write(`\n${ui("binaryPrerequisiteDryRun")}\n`);
392
+ return { proceed: false, updated: false };
393
+ }
394
+ return { proceed: true, updated: false };
395
+ }
396
+ if (!(await approve(commandOptions, ui("applyBinaryPrerequisite")))) {
397
+ process.stdout.write(`${ui("binaryPrerequisiteDeclined")}\n`);
398
+ return { proceed: false, updated: false };
399
+ }
400
+ const applied = parseSelfUpdateOutput((await runCapture(
401
+ akBinary, selfUpdateJsonApplyArgs("beta"),
402
+ )).stdout);
403
+ if (!applied.applied) throw new Error(ui("binaryNoChange"));
404
+ installedAkVersion = (await runCapture(akBinary, ["--version"])).stdout.trim();
405
+ const verifiedVersion = installedAkVersion.replace(/^ak\s+/i, "");
406
+ if (verifiedVersion !== applied.latest_version && verifiedVersion !== `v${applied.latest_version}`) {
407
+ throw new Error(`beta binary verification failed: expected ${applied.latest_version}, got ${verifiedVersion}`);
408
+ }
409
+ process.stdout.write(`${ui("binaryPrerequisiteComplete", {
410
+ version: verifiedVersion,
411
+ })}\n`);
412
+ return { proceed: true, updated: true };
413
+ }
414
+
415
+ async function install(commandOptions, allowBack = false) {
416
+ let scope;
417
+ let route;
418
+ while (true) {
419
+ const scopeWasPrompted = needsScopePrompt(commandOptions);
420
+ const routeCanGoBack = allowBack || scopeWasPrompted;
421
+ scope = await selectScope("install", commandOptions, allowBack);
422
+ if (scope === BACK) return BACK;
423
+ const config = scope.project ? await readProjectConfig(scope.project) : null;
424
+ route = await walkSelections([
425
+ { key: "kit", select: () => selectKit(commandOptions, config, routeCanGoBack, allowBack) },
426
+ {
427
+ key: "target",
428
+ select: (values) => selectTarget(
429
+ commandOptions, config, INSTALL_TARGETS, "targetPrompt", routeCanGoBack, allowBack,
430
+ { kit: kitName(values.kit) },
431
+ ),
432
+ },
433
+ { key: "channel", select: () => selectChannel(commandOptions, config, routeCanGoBack, allowBack) },
434
+ ]);
435
+ if (route !== BACK) break;
436
+ if (!scopeWasPrompted) return BACK;
437
+ }
438
+ const selection = { ...scope, ...route };
439
+ const installSelections = splitTargetSpec(selection.target)
440
+ .map((target) => ({ ...selection, target }));
441
+
442
+ printSection(ui("installPlan"));
443
+ for (const targetSelection of installSelections) {
444
+ printPlan(installArgs(targetSelection), targetSelection.project);
445
+ }
446
+ if (commandOptions.dryRun) {
447
+ process.stdout.write(`\n${ui("dryRunFiles")}\n`);
448
+ return;
449
+ }
450
+ if (selection.channel === "beta") {
451
+ const betaBinary = await prepareBetaBinary(commandOptions);
452
+ if (!betaBinary.proceed) return;
453
+ }
454
+ if (!(await approve(commandOptions, ui("runAk")))) {
455
+ process.stdout.write(`${ui("cancelledFiles")}\n`);
456
+ return;
457
+ }
458
+
459
+ for (const targetSelection of installSelections) {
460
+ const cwd = targetSelection.project || process.cwd();
461
+ try {
462
+ await runAkCommand(installArgs(targetSelection), { cwd });
463
+ } catch (error) {
464
+ if (!requiresForceConsent(error)) throw error;
465
+ warning(ui(targetSelection.global ? "globalForceWarning" : "projectForceWarning", {
466
+ target: targetSelection.target,
467
+ }));
468
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
469
+ error.message = `${error.message}\n${ui("forceNeedsConsent")}`;
470
+ throw error;
471
+ }
472
+ if (!(await confirm(ui("confirmForceInstall"), false))) {
473
+ process.stdout.write(`${ui("forceInstallDeclined")}\n`);
474
+ return;
475
+ }
476
+ const forceArgs = installArgs(targetSelection, { force: true });
477
+ printSection(ui("forceInstallPlan"));
478
+ printPlan(forceArgs, targetSelection.project);
479
+ await runAkCommand(forceArgs, { cwd });
480
+ }
481
+ }
482
+ if (selection.project && !commandOptions.noSave) {
483
+ const path = await writeProjectConfig(selection.project, selection);
484
+ process.stdout.write(`${ui("savedChoice", { path })}\n`);
485
+ }
486
+ if (selection.project) {
487
+ try {
488
+ await run(akBinary, ["projects", "add", selection.project, "--yes"]);
489
+ } catch (error) {
490
+ process.stderr.write(`${ui("projectRegistrationFailed", { message: error.message })}\n`);
491
+ }
492
+ }
493
+ process.stdout.write(`${ui("installComplete")}\n`);
494
+ }
495
+
496
+ async function selfUpdate(commandOptions, allowBack = false) {
497
+ const channel = await selectChannel(commandOptions, null, allowBack);
498
+ if (channel === BACK) return BACK;
499
+ printSection(ui("binaryCheck"));
500
+ const check = parseSelfUpdateOutput((await runCapture(
501
+ akBinary, selfUpdateJsonCheckArgs(channel),
502
+ )).stdout);
503
+ process.stdout.write(`${ui("binaryVersions", {
504
+ current: check.current_version,
505
+ latest: check.latest_version,
506
+ channel: check.channel || channel,
507
+ })}\n`);
508
+ const classification = classifySelfUpdate(check);
509
+ if (classification === "current") {
510
+ process.stdout.write(`${ui("binaryAlreadyCurrent")}\n`);
511
+ return;
512
+ }
513
+ if (classification === "downgrade") {
514
+ await downgradeBinary(commandOptions, check);
515
+ return;
516
+ }
517
+ if (classification !== "update") {
518
+ throw new Error(check.message || `ak self-update status: ${check.status}`);
519
+ }
520
+
521
+ const applyArgs = selfUpdateJsonApplyArgs(channel);
522
+ printSection(ui("binaryPlan"));
523
+ printPlan(selfUpdateApplyArgs(channel));
524
+ if (commandOptions.dryRun) {
525
+ process.stdout.write(`\n${ui("dryRunBinary")}\n`);
526
+ return;
527
+ }
528
+ if (!(await approve(commandOptions, ui("applyBinary")))) {
529
+ process.stdout.write(`${ui("cancelledBinary")}\n`);
530
+ return;
531
+ }
532
+ const applied = parseSelfUpdateOutput((await runCapture(akBinary, applyArgs)).stdout);
533
+ process.stdout.write(`${applied.applied ? ui("binaryComplete") : ui("binaryNoChange")}\n`);
534
+ }
535
+
536
+ function printDowngradePlan(channel, version, installDir) {
537
+ if (process.platform === "win32") {
538
+ process.stdout.write(` $env:AK_CHANNEL='${channel}'; $env:AK_VERSION='${version}'; irm ${windowsInstallerUrl} | iex\n`);
539
+ return;
540
+ }
541
+ process.stdout.write(` curl -fsSL ${installerUrl} | ${formatCommand("env", [
542
+ `AK_CHANNEL=${channel}`,
543
+ `AK_VERSION=${version}`,
544
+ `AK_INSTALL_DIR=${installDir}`,
545
+ "sh",
546
+ ])}\n`);
547
+ }
548
+
549
+ async function resolveAkInstallDir() {
550
+ if (process.platform === "win32") return null;
551
+ const located = akBinary.includes("/")
552
+ ? resolve(akBinary)
553
+ : (await runCapture("which", [akBinary])).stdout.split("\n")[0];
554
+ return dirname(await realpath(located));
555
+ }
556
+
557
+ async function applyOfficialInstaller(channel, version, installDir) {
558
+ if (process.platform === "win32") {
559
+ const script = `$env:AK_CHANNEL='${channel}'; $env:AK_VERSION='${version}'; irm ${windowsInstallerUrl} | iex`;
560
+ await run("powershell.exe", ["-NoProfile", "-Command", script]);
561
+ return;
562
+ }
563
+ await runPipeline(
564
+ process.env.AK_HELPER_CURL_BIN || "curl",
565
+ ["-fsSL", installerUrl],
566
+ process.env.AK_HELPER_SH_BIN || "sh",
567
+ [],
568
+ { targetEnv: { AK_CHANNEL: channel, AK_VERSION: version, AK_INSTALL_DIR: installDir } },
569
+ );
570
+ }
571
+
572
+ async function downgradeBinary(commandOptions, check) {
573
+ const version = assertInstallerVersion(check.latest_version);
574
+ const channel = check.channel || commandOptions.channel;
575
+ const installDir = await resolveAkInstallDir();
576
+ process.stderr.write(`${ui("binaryDowngradeWarning", {
577
+ channel,
578
+ latest: version,
579
+ current: check.current_version,
580
+ })}\n`);
581
+ process.stderr.write(`${ui("binaryDowngradeRisk")}\n`);
582
+ printSection(ui("downgradePlan"));
583
+ printDowngradePlan(channel, version, installDir);
584
+ if (commandOptions.dryRun) {
585
+ process.stdout.write(`\n${ui("dryRunBinary")}\n`);
586
+ return;
587
+ }
588
+
589
+ let approved = false;
590
+ if (process.stdin.isTTY && process.stdout.isTTY) {
591
+ approved = commandOptions.allowDowngrade && commandOptions.yes
592
+ ? true
593
+ : await confirm(ui("confirmDowngrade", {
594
+ current: check.current_version,
595
+ latest: version,
596
+ }), false);
597
+ } else if (commandOptions.allowDowngrade && commandOptions.yes) {
598
+ approved = true;
599
+ } else {
600
+ throw new Error(ui("downgradeNeedsConsent"));
601
+ }
602
+ if (!approved) {
603
+ process.stdout.write(`${ui("cancelledDowngrade")}\n`);
604
+ return;
605
+ }
606
+
607
+ await applyOfficialInstaller(channel, version, installDir);
608
+ const verified = (await runCapture(akBinary, ["--version"])).stdout;
609
+ const installedVersion = verified.trim().replace(/^ak\s+/, "");
610
+ if (installedVersion !== version && installedVersion !== `v${version}`) {
611
+ throw new Error(`downgrade verification failed: expected ${version}`);
612
+ }
613
+ process.stdout.write(`${ui("downgradeComplete", { version })}\n`);
614
+ }
615
+
616
+ async function update(commandOptions, allowBack = false) {
617
+ let scope;
618
+ let config;
619
+ let channel;
620
+
621
+ while (true) {
622
+ const scopeWasPrompted = needsScopePrompt(commandOptions);
623
+ const routeCanGoBack = allowBack || scopeWasPrompted;
624
+ scope = await selectScope("update", commandOptions, allowBack);
625
+ if (scope === BACK) return BACK;
626
+ config = scope.project ? await readProjectConfig(scope.project) : null;
627
+
628
+ if (
629
+ scope.project &&
630
+ !commandOptions.target &&
631
+ !commandOptions.runtime &&
632
+ config?.target &&
633
+ !targetSpecIsSupported(config.target, UPDATE_TARGETS) &&
634
+ (!process.stdin.isTTY || !process.stdout.isTTY)
635
+ ) {
636
+ throw new Error(ui("savedTargetNeedsUpdateRuntime", {
637
+ target: config.target,
638
+ supported: supportedList(UPDATE_TARGETS),
639
+ }));
640
+ }
641
+
642
+ if (scope.binaryOnly) {
643
+ channel = await selectChannel(commandOptions, config, routeCanGoBack, allowBack);
644
+ if (channel !== BACK) break;
645
+ } else {
646
+ const route = await walkSelections([
647
+ { key: "kit", select: () => selectKit(commandOptions, config, routeCanGoBack, allowBack) },
648
+ { key: "channel", select: () => selectChannel(commandOptions, config, routeCanGoBack, allowBack) },
649
+ {
650
+ key: "target",
651
+ select: (values) => selectTarget(
652
+ commandOptions, config, UPDATE_TARGETS, "updateTargetPrompt", routeCanGoBack, allowBack,
653
+ { kit: kitName(values.kit) },
654
+ ),
655
+ },
656
+ ]);
657
+ if (route !== BACK) {
658
+ channel = route.channel;
659
+ scope = { ...scope, ...route };
660
+ break;
661
+ }
662
+ }
663
+
664
+ if (!scopeWasPrompted) return BACK;
665
+ }
666
+
667
+ if (scope.binaryOnly) {
668
+ return selfUpdate({ ...commandOptions, channel });
669
+ }
670
+
671
+ const selection = {
672
+ ...scope,
673
+ channel,
674
+ };
675
+ const updateSelections = selection.global
676
+ ? [selection]
677
+ : splitTargetSpec(selection.target).map((target) => ({ ...selection, target }));
678
+ if (selection.global) warning(ui("globalUpdateSafety"));
679
+ if (channel === "beta") {
680
+ const betaBinary = await prepareBetaBinary(commandOptions, { requiresPreview: true });
681
+ if (!betaBinary.proceed) return;
682
+ }
683
+
684
+ printSection(ui("updatePreview"));
685
+ for (const targetSelection of updateSelections) {
686
+ if (updateSelections.length > 1) {
687
+ process.stdout.write(`${colorText(targetSelection.target, "target")}\n`);
688
+ }
689
+ await runAkCommand(updatePreviewArgs(targetSelection), {
690
+ cwd: targetSelection.project || process.cwd(),
691
+ });
692
+ }
693
+ printSection(ui("applyPlan"));
694
+ for (const targetSelection of updateSelections) {
695
+ printPlan(updateApplyArgs(targetSelection), targetSelection.project);
696
+ }
697
+ if (commandOptions.dryRun) {
698
+ process.stdout.write(`\n${ui("dryRunFiles")}\n`);
699
+ return;
700
+ }
701
+ if (!(await approve(commandOptions, ui("applyKit")))) {
702
+ process.stdout.write(`${ui("cancelledUpdate")}\n`);
703
+ return;
704
+ }
705
+
706
+ for (const targetSelection of updateSelections) {
707
+ await runAkCommand(updateApplyArgs(targetSelection), {
708
+ cwd: targetSelection.project || process.cwd(),
709
+ });
710
+ }
711
+ if (selection.project && !commandOptions.noSave) {
712
+ await writeProjectConfig(selection.project, selection);
713
+ }
714
+ process.stdout.write(`${ui("updateComplete")}\n`);
715
+ }
716
+
717
+ function updateAllPreviewArgs(candidate, channel) {
718
+ if (candidate.kind === "global") {
719
+ return globalUpdatePreviewArgs(channel, candidate.runtimes, candidate.kit);
720
+ }
721
+ return projectUpdatePreviewArgs(candidate.path, candidate.runtime, channel, candidate.kit);
722
+ }
723
+
724
+ function updateAllApplyArgs(candidate, channel) {
725
+ if (candidate.kind === "global") {
726
+ return globalUpdateApplyArgs(channel, candidate.runtimes, candidate.kit);
727
+ }
728
+ return projectUpdateApplyArgs(candidate.path, candidate.runtime, channel, candidate.kit);
729
+ }
730
+
731
+ async function askDeepScanRoots() {
732
+ return [await askDirectory(ui("deepScanRootsPrompt"), { root: homedir() })];
733
+ }
734
+
735
+ function buildUpdateCandidates(discovery, globalInstalls) {
736
+ const projects = discovery.projects.flatMap((project) => project.installs.map(({ kit, runtime }) => ({
737
+ ...project,
738
+ id: `${project.id}:${kit}:${runtime}`,
739
+ kit,
740
+ runtime,
741
+ label: ui("projectCandidate", {
742
+ name: project.name,
743
+ path: project.path,
744
+ runtime,
745
+ kit: kitName(kit),
746
+ }),
747
+ })));
748
+ return {
749
+ registered: projects.filter((project) => project.sources.includes("registry")),
750
+ unregistered: projects.filter((project) => !project.sources.includes("registry")),
751
+ other: globalInstalls.map(({ kit, runtimes }) => ({
752
+ id: `global:${kit}`,
753
+ kind: "global",
754
+ kit,
755
+ runtimes,
756
+ label: ui("globalCandidate", { kit: kitName(kit), runtimes: runtimes.join(", ") }),
757
+ })),
758
+ };
759
+ }
760
+
761
+ function printCandidateGroup(title, candidates) {
762
+ process.stdout.write(`\n${colorText(title, "group")}\n`);
763
+ for (const candidate of candidates) {
764
+ process.stdout.write(` - ${colorText(candidate.label, "target")}\n`);
765
+ }
766
+ }
767
+
768
+ function printUpdateInventory(groups) {
769
+ printSection(ui("inventory"));
770
+ if (groups.registered.length > 0) {
771
+ printCandidateGroup(ui("registeredProjects"), groups.registered);
772
+ } else {
773
+ process.stdout.write(` ${ui("noProjectsInRegistry")}\n`);
774
+ }
775
+ if (groups.unregistered.length > 0) {
776
+ printCandidateGroup(ui("unregisteredProjects"), groups.unregistered);
777
+ }
778
+ if (groups.other.length > 0) printCandidateGroup(ui("otherInstalls"), groups.other);
779
+ }
780
+
781
+ function orderUpdateCandidates(candidates) {
782
+ const priority = { global: 0, project: 1 };
783
+ return [...candidates].sort((left, right) => priority[left.kind] - priority[right.kind]);
784
+ }
785
+
786
+ async function loadUpdateInventory(commandOptions, deepScanRoots) {
787
+ const registry = await withSpinner(
788
+ ui("loadingRegistry"),
789
+ ui("loadedRegistry"),
790
+ () => runCapture(akBinary, ["projects", "list", "--json"]),
791
+ );
792
+ const discover = () => discoverProjectCandidates({
793
+ registryOutput: registry.stdout,
794
+ deepScanRoots,
795
+ maxDepth: commandOptions.maxDepth,
796
+ excludes: commandOptions.excludes,
797
+ supportedRuntimes: UPDATE_TARGETS,
798
+ });
799
+ const discovery = deepScanRoots.length > 0
800
+ ? await withSpinner(ui("deepScanning"), ui("deepScanComplete"), discover)
801
+ : await discover();
802
+ const globalInstalls = await discoverGlobalKitInstalls({
803
+ akHome: process.env.AGENTKIT_HOME,
804
+ supportedRuntimes: UPDATE_TARGETS,
805
+ kits: KITS,
806
+ });
807
+ return { discovery, groups: buildUpdateCandidates(discovery, globalInstalls) };
808
+ }
809
+
810
+ async function updateAll(commandOptions, allowBack = false) {
811
+ while (true) {
812
+ const channel = await selectChannel(commandOptions, null, allowBack, allowBack);
813
+ if (channel === BACK) return BACK;
814
+ let deepScanRoots = [...commandOptions.deepScanRoots];
815
+ let selected;
816
+ let scanBaselinePaths = null;
817
+ while (!selected) {
818
+ const { discovery, groups } = await loadUpdateInventory(commandOptions, deepScanRoots);
819
+ for (const warning of discovery.warnings) {
820
+ process.stderr.write(`${ui("discoveryWarning", { message: warning })}\n`);
821
+ }
822
+ printUpdateInventory(groups);
823
+ const candidates = [...groups.registered, ...groups.unregistered, ...groups.other];
824
+ const projectCandidates = [...groups.registered, ...groups.unregistered];
825
+ const projectCount = new Set(projectCandidates.map((candidate) => candidate.path)).size;
826
+ if (scanBaselinePaths) {
827
+ const addedPaths = new Set(projectCandidates
828
+ .map((candidate) => candidate.path)
829
+ .filter((path) => !scanBaselinePaths.has(path)));
830
+ process.stdout.write(`${ui("deepScanResult", { count: addedPaths.size })}\n`);
831
+ scanBaselinePaths = null;
832
+ }
833
+
834
+ if (!process.stdin.isTTY || !process.stdout.isTTY || commandOptions.yes) {
835
+ selected = candidates;
836
+ break;
837
+ }
838
+ let reloadInventory = false;
839
+ while (!selected && !reloadInventory) {
840
+ const actionChoices = [
841
+ { label: ui("updateEverything", { count: candidates.length }), value: "all" },
842
+ {
843
+ label: ui("updateAllProjects", { count: projectCount }),
844
+ value: "projects",
845
+ disabled: projectCandidates.length === 0 ? ui("noUpdateCandidates") : false,
846
+ },
847
+ { label: ui("chooseUpdates"), value: "choose" },
848
+ { label: ui("customDeepScanAction"), value: "custom-deep-scan" },
849
+ ];
850
+ if (allowBack) actionChoices.push({ label: ui("updateAllBack"), value: BACK });
851
+ const action = await choose(ui("selectUpdateAll"), actionChoices);
852
+ if (action === BACK) break;
853
+ if (action === "custom-deep-scan") {
854
+ scanBaselinePaths = new Set(projectCandidates.map((candidate) => candidate.path));
855
+ deepScanRoots = [...new Set([...deepScanRoots, ...await askDeepScanRoots()])];
856
+ reloadInventory = true;
857
+ continue;
858
+ }
859
+ if (action === "all") {
860
+ selected = candidates;
861
+ continue;
862
+ }
863
+ if (action === "projects") {
864
+ selected = projectCandidates;
865
+ continue;
866
+ }
867
+ const ids = await multiChooseWithBack(
868
+ ui("selectUpdateAll"),
869
+ candidates.map((candidate) => ({ label: candidate.label, value: candidate.id })),
870
+ candidates.map((candidate) => candidate.id),
871
+ ui("backFromTargetSelection"),
872
+ );
873
+ if (ids === BACK) continue;
874
+ const selectedAction = await choose(ui("confirmSelectedUpdates", { count: ids.length }), [
875
+ { label: ui("previewSelectedUpdates", { count: ids.length }), value: "continue" },
876
+ { label: ui("back"), value: BACK },
877
+ ]);
878
+ if (selectedAction === BACK) continue;
879
+ selected = candidates.filter((candidate) => ids.includes(candidate.id));
880
+ }
881
+ if (reloadInventory) continue;
882
+ if (!selected) break;
883
+ }
884
+ if (!selected) continue;
885
+ if (selected.length === 0) throw new Error(ui("noUpdateCandidates"));
886
+ selected = orderUpdateCandidates(selected);
887
+ if (selected.some((candidate) => candidate.kind === "global")) {
888
+ warning(ui("globalUpdateSafety"));
889
+ }
890
+ if (channel === "beta" && selected.length > 0) {
891
+ const betaBinary = await prepareBetaBinary(commandOptions, { requiresPreview: true });
892
+ if (!betaBinary.proceed) return;
893
+ }
894
+
895
+ printSection(ui("updateAllPreview"));
896
+ for (const candidate of selected) {
897
+ process.stdout.write(`\n${colorText(candidate.label, "target")}\n`);
898
+ await runAkCommand(updateAllPreviewArgs(candidate, channel));
899
+ }
900
+ printSection(ui("updateAllApplyPlan"));
901
+ for (const candidate of selected) printPlan(updateAllApplyArgs(candidate, channel));
902
+ if (commandOptions.dryRun) {
903
+ process.stdout.write(`\n${ui("dryRunFiles")}\n`);
904
+ return;
905
+ }
906
+ if (!(await approve(commandOptions, ui("applyAll")))) {
907
+ process.stdout.write(`${ui("cancelledAll")}\n`);
908
+ return;
909
+ }
910
+ for (const [index, candidate] of selected.entries()) {
911
+ process.stdout.write(`${ui("updateProgress", {
912
+ current: index + 1,
913
+ total: selected.length,
914
+ label: candidate.label,
915
+ })}\n`);
916
+ await runAkCommand(updateAllApplyArgs(candidate, channel));
917
+ }
918
+ process.stdout.write(`${ui("updateAllComplete")}\n`);
919
+ return;
920
+ }
921
+ }
922
+
923
+ async function exportKit(commandOptions, allowBack = false) {
924
+ const choices = [
925
+ { label: ui("agyExport"), value: "agy" },
926
+ { label: ui("portableExport"), value: "portable" },
927
+ ].filter((choice) => EXPORT_TARGETS.has(choice.value));
928
+ const route = await walkSelections([
929
+ {
930
+ key: "kit",
931
+ select: () => selectKit(commandOptions, null, allowBack),
932
+ },
933
+ {
934
+ key: "target",
935
+ select: () => commandOptions.target || chooseLocalized(
936
+ allowBack,
937
+ ui("exportTargetPrompt"),
938
+ choices,
939
+ ),
940
+ },
941
+ {
942
+ key: "channel",
943
+ select: () => selectChannel(commandOptions, null, allowBack),
944
+ },
945
+ ]);
946
+ if (route === BACK) return BACK;
947
+ const { kit, target, channel } = route;
948
+ if (target === "agy" && commandOptions.target && !commandOptions.global) {
949
+ throw new Error("agy export requires --global");
950
+ }
951
+ if (target === "agy" && commandOptions.out) {
952
+ throw new Error("agy export uses --global, not --out");
953
+ }
954
+ if (target === "portable" && commandOptions.global) {
955
+ throw new Error("portable export uses --out, not --global");
956
+ }
957
+ let out = null;
958
+ if (target === "portable") {
959
+ out = resolve(commandOptions.out || await ask(ui("exportDirectory")));
960
+ if (isUnsafeProjectPath(out)) {
961
+ throw new Error("portable export output cannot be the filesystem root or home directory");
962
+ }
963
+ }
964
+ const selection = { kit, target, channel, global: target === "agy", out };
965
+ const args = exportArgs(selection);
966
+ printSection(ui("exportPlan"));
967
+ printPlan(args);
968
+ if (commandOptions.dryRun) {
969
+ process.stdout.write(`\n${ui("dryRunFiles")}\n`);
970
+ return;
971
+ }
972
+ if (channel === "beta") {
973
+ const betaBinary = await prepareBetaBinary(commandOptions);
974
+ if (!betaBinary.proceed) return;
975
+ }
976
+ if (!(await approve(commandOptions, ui("runExport")))) {
977
+ process.stdout.write(`${ui("cancelledFiles")}\n`);
978
+ return;
979
+ }
980
+ await runAkCommand(args);
981
+ process.stdout.write(`${ui("exportComplete")}\n`);
982
+ }
983
+
984
+ async function doctor(options) {
985
+ const cwd = options.project ? await resolveProjectPath(options.project) : process.cwd();
986
+ await runAkCommand(["doctor", "--exit-on-fail", "--verbose"], { cwd });
987
+ }
988
+
989
+ async function main() {
990
+ const options = parseArgs(process.argv.slice(2));
991
+ if (options.version) {
992
+ process.stdout.write(`${metadata.version}\n`);
993
+ return;
994
+ }
995
+ if (options.help || options.command === "help") {
996
+ usage(options.language || "en");
997
+ return;
998
+ }
999
+ if (!options.command && (!process.stdin.isTTY || !process.stdout.isTTY)) {
1000
+ throw new Error("interactive input is unavailable; pass explicit flags and --yes");
1001
+ }
1002
+
1003
+ activeLanguage = await selectLanguage(options);
1004
+ setPromptCopy({ cancelled: ui("promptCancelled") });
1005
+
1006
+ const interactiveRoot = !options.command;
1007
+ const languageCanChange = interactiveRoot && !options.language;
1008
+ installedAkVersion = await ensureAk(akBinary);
1009
+ if (interactiveRoot) showCurrentBinary();
1010
+ while (true) {
1011
+ if (!options.command) {
1012
+ const command = await chooseCommand(languageCanChange);
1013
+ if (command === BACK) {
1014
+ activeLanguage = await selectLanguage(options, true);
1015
+ setPromptCopy({ cancelled: ui("promptCancelled") });
1016
+ showCurrentBinary();
1017
+ continue;
1018
+ }
1019
+ options.command = command;
1020
+ }
1021
+ activeAction = options.command;
1022
+ validateForCommand(options);
1023
+ let result;
1024
+ switch (options.command) {
1025
+ case "install":
1026
+ result = await install(options, interactiveRoot);
1027
+ break;
1028
+ case "update":
1029
+ result = await update(options, interactiveRoot);
1030
+ break;
1031
+ case "self-update":
1032
+ result = await selfUpdate(options, interactiveRoot);
1033
+ break;
1034
+ case "update-all":
1035
+ result = await updateAll(options, interactiveRoot);
1036
+ break;
1037
+ case "export":
1038
+ result = await exportKit(options, interactiveRoot);
1039
+ break;
1040
+ case "doctor":
1041
+ result = await doctor(options);
1042
+ break;
1043
+ default:
1044
+ usage(activeLanguage);
1045
+ throw new Error(`unsupported command: ${options.command}`);
1046
+ }
1047
+ if (interactiveRoot && result === BACK) {
1048
+ options.command = null;
1049
+ continue;
1050
+ }
1051
+ break;
1052
+ }
1053
+ finishInteractive(ui("done"));
1054
+ }
1055
+
1056
+ async function offerGitHubIssue(error) {
1057
+ if (!isReportableAkError(error, akBinary)) return;
1058
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return;
1059
+ const repository = resolveIssueRepository();
1060
+ const report = buildIssueReport({
1061
+ error,
1062
+ helperVersion: metadata.version,
1063
+ action: activeAction,
1064
+ language: activeLanguage,
1065
+ cwd: error.command?.cwd || process.cwd(),
1066
+ });
1067
+ try {
1068
+ try {
1069
+ await checkIssueRepository({ repository, ghBinary });
1070
+ } catch {
1071
+ warning(ui("issueRepoUnavailable", { repo: repository }));
1072
+ process.stderr.write(`${ui("issueRepoSetup")}\n`);
1073
+ return;
1074
+ }
1075
+ if (!(await confirm(ui("createIssue", { repo: repository }), false))) {
1076
+ process.stdout.write(`${ui("issueSkipped")}\n`);
1077
+ return;
1078
+ }
1079
+ const duplicate = await findDuplicateIssue(report, { repository, ghBinary });
1080
+ if (duplicate) {
1081
+ process.stdout.write(`${ui("issueDuplicate", { url: duplicate.url })}\n`);
1082
+ return;
1083
+ }
1084
+ const url = await createGitHubIssue(report, { repository, ghBinary });
1085
+ process.stdout.write(`${ui("issueCreated", { url })}\n`);
1086
+ } catch (issueError) {
1087
+ process.stderr.write(`${ui("issueFailed", { message: issueError.message })}\n`);
1088
+ process.stderr.write(`${ui("issueManual")}\n`);
1089
+ }
1090
+ }
1091
+
1092
+ try {
1093
+ await main();
1094
+ } catch (error) {
1095
+ if (error instanceof PromptCancelledError) {
1096
+ process.exitCode = 0;
1097
+ } else {
1098
+ const exitCode = error.exitCode || 1;
1099
+ process.stderr.write(`${ui("error", { message: error.message })}\n`);
1100
+ try {
1101
+ await offerGitHubIssue(error);
1102
+ } catch (reportError) {
1103
+ if (!(reportError instanceof PromptCancelledError)) {
1104
+ process.stderr.write(`${ui("issueFailed", { message: reportError.message })}\n`);
1105
+ }
1106
+ }
1107
+ process.exitCode = exitCode;
1108
+ }
1109
+ }