@thieung/agentkit-helper 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -28,7 +28,7 @@ In the TUI, use:
28
28
  - ↑/↓ to navigate
29
29
  - Space to select multiple runtimes
30
30
  - Enter to confirm
31
- - `← Back` to return to the previous step
31
+ - Esc to return to the previous step
32
32
 
33
33
  Choose a project or user/global scope, Engineer or Marketing Kit, one or more
34
34
  runtimes, and the Stable or Beta channel. AgentKit remains responsible for Kit
@@ -40,6 +40,12 @@ separate consent, defaulting to No. It retries with `--force` only after you
40
40
  choose Yes. Global update is preserve-only: user-modified files are skipped
41
41
  and the helper never adds `--force`.
42
42
 
43
+ Platform status: macOS is verified locally. Linux is a supported target for the
44
+ portable Node.js path. Native Windows PowerShell support is experimental until
45
+ it passes a real-machine smoke test; the repository includes Windows-oriented
46
+ command handling and CI coverage, but does not yet claim provider-backed E2E
47
+ verification.
48
+
43
49
  <details>
44
50
  <summary><strong>Advanced CLI usage</strong></summary>
45
51
 
package/README.vi.md CHANGED
@@ -27,7 +27,7 @@ Trong TUI, dùng:
27
27
  - ↑/↓ để di chuyển
28
28
  - Space để chọn nhiều runtime
29
29
  - Enter để xác nhận
30
- - `← Quay lại` để trở về bước trước
30
+ - Esc để trở về bước trước
31
31
 
32
32
  Bạn chỉ cần chọn scope project hoặc user/global, Engineer hoặc Marketing Kit,
33
33
  một hay nhiều runtime và release channel Stable hoặc Beta. AgentKit vẫn quản
@@ -38,6 +38,11 @@ tồn tại hoặc có drift, TUI hiển thị WARNING và hỏi consent riêng,
38
38
  Chỉ khi chọn Yes, helper mới retry bằng `--force`. Global update luôn dùng
39
39
  preserve-only: file do user chỉnh sửa được bỏ qua và helper không thêm `--force`.
40
40
 
41
+ Trạng thái platform: macOS đã được verify local. Linux là target được hỗ trợ
42
+ qua code path Node.js portable. Native Windows PowerShell đang ở mức
43
+ experimental cho đến khi được smoke-test trên máy thật; repository đã có xử lý
44
+ command và CI dành cho Windows nhưng chưa claim provider-backed E2E.
45
+
41
46
  <details>
42
47
  <summary><strong>Cách dùng CLI nâng cao</strong></summary>
43
48
 
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  exportArgs,
19
19
  formatCommand,
20
+ formatEnvironmentAssignments,
20
21
  globalUpdateApplyArgs,
21
22
  globalUpdatePreviewArgs,
22
23
  installArgs,
@@ -45,6 +46,12 @@ import {
45
46
  } from "../lib/github-issue.mjs";
46
47
  import { t } from "../lib/i18n.mjs";
47
48
  import { isUnsafeProjectPath, resolveProjectPath } from "../lib/project.mjs";
49
+ import {
50
+ classifyPiProfiles,
51
+ parsePiProfileInventory,
52
+ piProfileManagerInvocation,
53
+ piProfileProcessOptions,
54
+ } from "../lib/pi-profiles.mjs";
48
55
  import { BACK, walkSelections } from "../lib/navigation.mjs";
49
56
  import {
50
57
  ask,
@@ -78,11 +85,15 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
78
85
  const metadata = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
79
86
  const akBinary = process.env.AK_HELPER_AK_BIN || "ak";
80
87
  const ghBinary = process.env.AK_HELPER_GH_BIN || "gh";
88
+ const piProfileManager = piProfileManagerInvocation(
89
+ process.env.AK_HELPER_PPM_BIN || "pi-profile-manager",
90
+ );
81
91
  const installerUrl = "https://agentkit.best/install.sh";
82
92
  const windowsInstallerUrl = "https://agentkit.best/install.ps1";
83
93
  let activeLanguage = "en";
84
94
  let activeAction = null;
85
95
  let installedAkVersion = "";
96
+ let binaryUpdateAvailable = false;
86
97
 
87
98
  function ui(key, values) {
88
99
  return t(activeLanguage, key, values);
@@ -172,38 +183,74 @@ Target groups:
172
183
  `);
173
184
  }
174
185
 
175
- function printPlan(args, cwd) {
186
+ function printPlan(args, cwd, envOverrides = {}) {
176
187
  const suffix = cwd && cwd !== process.cwd() ? ` (cwd: ${cwd})` : "";
177
- process.stdout.write(`${colorText(` ${formatCommand(akBinary, args)}${suffix}`, "command")}\n`);
188
+ const environment = formatEnvironmentAssignments(envOverrides);
189
+ const prefix = environment ? `${environment} ` : "";
190
+ process.stdout.write(`${colorText(` ${prefix}${formatCommand(akBinary, args)}${suffix}`, "command")}\n`);
178
191
  }
179
192
 
180
193
  function printSection(message) {
181
194
  process.stdout.write(`\n${colorText(message, "section")}\n`);
182
195
  }
183
196
 
184
- async function runAkCommand(args, { cwd = process.cwd() } = {}) {
197
+ async function runAkCommand(
198
+ args,
199
+ { cwd = process.cwd(), envOverrides = {}, envUnset = [] } = {},
200
+ ) {
185
201
  const result = await withSpinner(
186
202
  ui("runningAkCommand"),
187
203
  ui("akCommandComplete"),
188
- () => runCapture(akBinary, args, { cwd }),
204
+ () => runCapture(akBinary, args, { cwd, envOverrides, envUnset }),
189
205
  );
190
206
  if (result.stdout) process.stdout.write(`${result.stdout}\n`);
191
207
  if (result.stderr) process.stderr.write(`${result.stderr}\n`);
192
208
  }
193
209
 
210
+ async function checkInteractiveBinaryUpdate() {
211
+ const channel = releaseChannelForVersion(installedAkVersion) || "stable";
212
+ try {
213
+ const result = await withSpinner(
214
+ ui("checkingBinaryUpdate"),
215
+ ui("checkedBinaryUpdate"),
216
+ () => runCapture(akBinary, selfUpdateJsonCheckArgs(channel)),
217
+ );
218
+ const check = parseSelfUpdateOutput(result.stdout);
219
+ const classification = classifySelfUpdate(check);
220
+ binaryUpdateAvailable = classification === "update";
221
+ if (binaryUpdateAvailable) {
222
+ warning(`│ ${ui("binaryUpdateAvailable", {
223
+ current: check.current_version,
224
+ latest: check.latest_version,
225
+ channel: check.channel || channel,
226
+ })}`);
227
+ } else if (classification === "current") {
228
+ process.stdout.write(`${colorText(`│ ${ui("binaryUpToDate", {
229
+ version: check.current_version,
230
+ channel: check.channel || channel,
231
+ })}`, "binary")}\n`);
232
+ } else {
233
+ warning(`│ ${ui("binaryUpdateStatusUnknown", { status: check.status })}`);
234
+ }
235
+ } catch (error) {
236
+ binaryUpdateAvailable = false;
237
+ warning(`│ ${ui("binaryUpdateCheckFailed", { message: error.message })}`);
238
+ }
239
+ }
240
+
194
241
  async function chooseCommand(allowLanguageBack = false) {
195
242
  const choices = [
243
+ { label: ui("selfUpdateAction"), value: "self-update" },
196
244
  { label: ui("installAction"), value: "install" },
197
245
  { label: ui("updateAction"), value: "update" },
198
- { label: ui("selfUpdateAction"), value: "self-update" },
199
246
  { label: ui("updateAllAction"), value: "update-all" },
200
247
  { label: ui("exportAction"), value: "export" },
201
248
  { label: ui("doctorAction"), value: "doctor" },
202
249
  ];
203
- if (allowLanguageBack) {
204
- choices.push({ label: ui("backToLanguage"), value: BACK });
205
- }
206
- return choose(ui("commandPrompt"), choices);
250
+ const defaultIndex = binaryUpdateAvailable ? 0 : 1;
251
+ return allowLanguageBack
252
+ ? chooseWithBack(ui("commandPrompt"), choices, defaultIndex, ui("escapeBack"))
253
+ : choose(ui("commandPrompt"), choices, defaultIndex);
207
254
  }
208
255
 
209
256
  function kitName(kit) {
@@ -231,7 +278,7 @@ async function selectLanguage(options, forcePrompt = false) {
231
278
 
232
279
  function chooseLocalized(allowBack, message, choices, defaultIndex = 0) {
233
280
  return allowBack
234
- ? chooseWithBack(message, choices, defaultIndex, ui("back"))
281
+ ? chooseWithBack(message, choices, defaultIndex, ui("escapeBack"))
235
282
  : choose(message, choices, defaultIndex);
236
283
  }
237
284
 
@@ -331,8 +378,7 @@ async function selectTarget(
331
378
  disabled: true,
332
379
  });
333
380
  }
334
- const configuredTargets = splitTargetSpec(config?.target).filter((target) => targets.has(target));
335
- const initialValues = configuredTargets.length > 0 ? configuredTargets : ["codex"];
381
+ const initialValues = [];
336
382
  const selected = await (allowBack ? multiChooseWithBack : multiChoose)(
337
383
  ui(promptKey, promptValues),
338
384
  choices,
@@ -715,14 +761,14 @@ async function update(commandOptions, allowBack = false) {
715
761
  }
716
762
 
717
763
  function updateAllPreviewArgs(candidate, channel) {
718
- if (candidate.kind === "global") {
764
+ if (candidate.kind === "global" || candidate.kind === "profile") {
719
765
  return globalUpdatePreviewArgs(channel, candidate.runtimes, candidate.kit);
720
766
  }
721
767
  return projectUpdatePreviewArgs(candidate.path, candidate.runtime, channel, candidate.kit);
722
768
  }
723
769
 
724
770
  function updateAllApplyArgs(candidate, channel) {
725
- if (candidate.kind === "global") {
771
+ if (candidate.kind === "global" || candidate.kind === "profile") {
726
772
  return globalUpdateApplyArgs(channel, candidate.runtimes, candidate.kit);
727
773
  }
728
774
  return projectUpdateApplyArgs(candidate.path, candidate.runtime, channel, candidate.kit);
@@ -732,7 +778,12 @@ async function askDeepScanRoots() {
732
778
  return [await askDirectory(ui("deepScanRootsPrompt"), { root: homedir() })];
733
779
  }
734
780
 
735
- function buildUpdateCandidates(discovery, globalInstalls) {
781
+ function buildUpdateCandidates(
782
+ discovery,
783
+ globalInstalls,
784
+ piProfiles = [],
785
+ claimedProfileRuntimes = new Set(),
786
+ ) {
736
787
  const projects = discovery.projects.flatMap((project) => project.installs.map(({ kit, runtime }) => ({
737
788
  ...project,
738
789
  id: `${project.id}:${kit}:${runtime}`,
@@ -745,16 +796,40 @@ function buildUpdateCandidates(discovery, globalInstalls) {
745
796
  kit: kitName(kit),
746
797
  }),
747
798
  })));
799
+ const profileCandidates = piProfiles.map((profile) => ({
800
+ id: `profile:${profile.runtime}:${profile.id}`,
801
+ kind: "profile",
802
+ kit: "engineer",
803
+ runtime: profile.runtime,
804
+ runtimes: [profile.runtime],
805
+ profile,
806
+ ...piProfileProcessOptions(profile),
807
+ label: ui("piProfileCandidate", {
808
+ profile: profile.id,
809
+ runtime: profile.runtime,
810
+ }),
811
+ }));
812
+ const ordinaryGlobalInstalls = globalInstalls
813
+ .map((install) => install.kit === "engineer"
814
+ ? {
815
+ ...install,
816
+ runtimes: install.runtimes.filter((runtime) => !claimedProfileRuntimes.has(runtime)),
817
+ }
818
+ : install)
819
+ .filter((install) => install.runtimes.length > 0);
748
820
  return {
749
821
  registered: projects.filter((project) => project.sources.includes("registry")),
750
822
  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
- })),
823
+ other: [
824
+ ...profileCandidates,
825
+ ...ordinaryGlobalInstalls.map(({ kit, runtimes }) => ({
826
+ id: `global:${kit}`,
827
+ kind: "global",
828
+ kit,
829
+ runtimes,
830
+ label: ui("globalCandidate", { kit: kitName(kit), runtimes: runtimes.join(", ") }),
831
+ })),
832
+ ],
758
833
  };
759
834
  }
760
835
 
@@ -779,7 +854,7 @@ function printUpdateInventory(groups) {
779
854
  }
780
855
 
781
856
  function orderUpdateCandidates(candidates) {
782
- const priority = { global: 0, project: 1 };
857
+ const priority = { profile: 0, global: 1, project: 2 };
783
858
  return [...candidates].sort((left, right) => priority[left.kind] - priority[right.kind]);
784
859
  }
785
860
 
@@ -804,7 +879,46 @@ async function loadUpdateInventory(commandOptions, deepScanRoots) {
804
879
  supportedRuntimes: UPDATE_TARGETS,
805
880
  kits: KITS,
806
881
  });
807
- return { discovery, groups: buildUpdateCandidates(discovery, globalInstalls) };
882
+ const profileRuntimeInstalls = new Set(globalInstalls
883
+ .filter((install) => install.kit === "engineer")
884
+ .flatMap((install) => install.runtimes)
885
+ .filter((runtime) => runtime === "pi" || runtime === "omp"));
886
+ let updateableProfiles = [];
887
+ const claimedProfileRuntimes = new Set();
888
+ try {
889
+ const inventory = await runCapture(piProfileManager.binary, [
890
+ ...piProfileManager.prefixArgs,
891
+ "profiles", "list", "--json",
892
+ ]);
893
+ const profiles = parsePiProfileInventory(inventory.stdout);
894
+ for (const profile of profiles.filter((profile) => profile.agentkitEnabled)) {
895
+ claimedProfileRuntimes.add(profile.runtime);
896
+ }
897
+ const classified = classifyPiProfiles(profiles);
898
+ updateableProfiles = classified.updateable;
899
+ for (const profile of classified.skipped) {
900
+ discovery.warnings.push(ui("piProfileSkipped", {
901
+ profile: profile.id,
902
+ reason: !profile.agentkitEnabled
903
+ ? ui("piProfileNoAgentKit")
904
+ : ui("piProfileNotSafe"),
905
+ }));
906
+ }
907
+ } catch (error) {
908
+ if (error.code !== "ENOENT") {
909
+ for (const runtime of profileRuntimeInstalls) claimedProfileRuntimes.add(runtime);
910
+ discovery.warnings.push(ui("piProfileDiscoveryUnavailable", { message: error.message }));
911
+ }
912
+ }
913
+ return {
914
+ discovery,
915
+ groups: buildUpdateCandidates(
916
+ discovery,
917
+ globalInstalls,
918
+ updateableProfiles,
919
+ claimedProfileRuntimes,
920
+ ),
921
+ };
808
922
  }
809
923
 
810
924
  async function updateAll(commandOptions, allowBack = false) {
@@ -847,8 +961,9 @@ async function updateAll(commandOptions, allowBack = false) {
847
961
  { label: ui("chooseUpdates"), value: "choose" },
848
962
  { label: ui("customDeepScanAction"), value: "custom-deep-scan" },
849
963
  ];
850
- if (allowBack) actionChoices.push({ label: ui("updateAllBack"), value: BACK });
851
- const action = await choose(ui("selectUpdateAll"), actionChoices);
964
+ const action = allowBack
965
+ ? await chooseWithBack(ui("selectUpdateAll"), actionChoices, 0, ui("escapeBack"))
966
+ : await choose(ui("selectUpdateAll"), actionChoices);
852
967
  if (action === BACK) break;
853
968
  if (action === "custom-deep-scan") {
854
969
  scanBaselinePaths = new Set(projectCandidates.map((candidate) => candidate.path));
@@ -871,10 +986,12 @@ async function updateAll(commandOptions, allowBack = false) {
871
986
  ui("backFromTargetSelection"),
872
987
  );
873
988
  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
- ]);
989
+ const selectedAction = await chooseWithBack(
990
+ ui("confirmSelectedUpdates", { count: ids.length }),
991
+ [{ label: ui("previewSelectedUpdates", { count: ids.length }), value: "continue" }],
992
+ 0,
993
+ ui("escapeBack"),
994
+ );
878
995
  if (selectedAction === BACK) continue;
879
996
  selected = candidates.filter((candidate) => ids.includes(candidate.id));
880
997
  }
@@ -884,7 +1001,7 @@ async function updateAll(commandOptions, allowBack = false) {
884
1001
  if (!selected) continue;
885
1002
  if (selected.length === 0) throw new Error(ui("noUpdateCandidates"));
886
1003
  selected = orderUpdateCandidates(selected);
887
- if (selected.some((candidate) => candidate.kind === "global")) {
1004
+ if (selected.some((candidate) => candidate.kind === "global" || candidate.kind === "profile")) {
888
1005
  warning(ui("globalUpdateSafety"));
889
1006
  }
890
1007
  if (channel === "beta" && selected.length > 0) {
@@ -895,10 +1012,12 @@ async function updateAll(commandOptions, allowBack = false) {
895
1012
  printSection(ui("updateAllPreview"));
896
1013
  for (const candidate of selected) {
897
1014
  process.stdout.write(`\n${colorText(candidate.label, "target")}\n`);
898
- await runAkCommand(updateAllPreviewArgs(candidate, channel));
1015
+ await runAkCommand(updateAllPreviewArgs(candidate, channel), candidate);
899
1016
  }
900
1017
  printSection(ui("updateAllApplyPlan"));
901
- for (const candidate of selected) printPlan(updateAllApplyArgs(candidate, channel));
1018
+ for (const candidate of selected) {
1019
+ printPlan(updateAllApplyArgs(candidate, channel), null, candidate.envOverrides);
1020
+ }
902
1021
  if (commandOptions.dryRun) {
903
1022
  process.stdout.write(`\n${ui("dryRunFiles")}\n`);
904
1023
  return;
@@ -913,7 +1032,7 @@ async function updateAll(commandOptions, allowBack = false) {
913
1032
  total: selected.length,
914
1033
  label: candidate.label,
915
1034
  })}\n`);
916
- await runAkCommand(updateAllApplyArgs(candidate, channel));
1035
+ await runAkCommand(updateAllApplyArgs(candidate, channel), candidate);
917
1036
  }
918
1037
  process.stdout.write(`${ui("updateAllComplete")}\n`);
919
1038
  return;
@@ -1006,7 +1125,10 @@ async function main() {
1006
1125
  const interactiveRoot = !options.command;
1007
1126
  const languageCanChange = interactiveRoot && !options.language;
1008
1127
  installedAkVersion = await ensureAk(akBinary);
1009
- if (interactiveRoot) showCurrentBinary();
1128
+ if (interactiveRoot) {
1129
+ showCurrentBinary();
1130
+ await checkInteractiveBinaryUpdate();
1131
+ }
1010
1132
  while (true) {
1011
1133
  if (!options.command) {
1012
1134
  const command = await chooseCommand(languageCanChange);
package/lib/commands.mjs CHANGED
@@ -101,6 +101,27 @@ function quote(value) {
101
101
  : `'${value.replaceAll("'", `'\\''`)}'`;
102
102
  }
103
103
 
104
- export function formatCommand(binary, args) {
105
- return [binary, ...args].map(quote).join(" ");
104
+ function quotePowerShell(value) {
105
+ return /^[A-Za-z0-9_./:@=\\-]+$/.test(value)
106
+ ? value
107
+ : `'${value.replaceAll("'", "''")}'`;
108
+ }
109
+
110
+ export function formatCommand(binary, args, { platform = process.platform } = {}) {
111
+ const quoteValue = platform === "win32" ? quotePowerShell : quote;
112
+ return [binary, ...args].map(quoteValue).join(" ");
113
+ }
114
+
115
+ export function formatEnvironmentAssignments(
116
+ values,
117
+ { platform = process.platform } = {},
118
+ ) {
119
+ if (platform === "win32") {
120
+ return Object.entries(values)
121
+ .map(([name, value]) => `$env:${name} = ${quotePowerShell(String(value))};`)
122
+ .join(" ");
123
+ }
124
+ return Object.entries(values)
125
+ .map(([name, value]) => `${name}=${quote(String(value))}`)
126
+ .join(" ");
106
127
  }
package/lib/i18n.mjs CHANGED
@@ -5,6 +5,12 @@ const messages = {
5
5
  languagePrompt: "Choose interface language / Chọn ngôn ngữ giao diện",
6
6
  commandPrompt: "What do you want to do?",
7
7
  currentBinary: "Current ak binary: {version} ({channel})",
8
+ checkingBinaryUpdate: "Checking for ak binary updates…",
9
+ checkedBinaryUpdate: "Checked ak binary updates.",
10
+ binaryUpdateAvailable: "UPDATE AVAILABLE: ak {current} → {latest} ({channel}). The update action is selected for you.",
11
+ binaryUpToDate: "UP TO DATE: ak {version} is the latest version on the {channel} channel.",
12
+ binaryUpdateStatusUnknown: "Could not determine binary update availability (status: {status}).",
13
+ binaryUpdateCheckFailed: "Could not check for ak binary updates: {message}. Continuing without blocking the helper.",
8
14
  installAction: "Install a Kit",
9
15
  updateAction: "Update a Kit",
10
16
  selfUpdateAction: "Update ak binary",
@@ -12,6 +18,7 @@ const messages = {
12
18
  exportAction: "Export Kit (not a runtime install)",
13
19
  doctorAction: "Run health checks",
14
20
  backToLanguage: "← Back to interface language",
21
+ escapeBack: "Esc: Back",
15
22
  chooseProject: "Choose a project directory",
16
23
  currentProjectScope: "Use current project ({path})",
17
24
  globalScope: "Use user/global Kit scope",
@@ -84,12 +91,17 @@ const messages = {
84
91
  unregisteredProjects: "Other discovered projects:",
85
92
  otherInstalls: "Global scope:",
86
93
  globalCandidate: "{kit} global scope ({runtimes})",
94
+ piProfileCandidate: "Engineer Kit — {runtime} profile: {profile}",
95
+ piProfileSkipped: "Pi Profile Manager skipped {profile}: {reason}",
96
+ piProfileNoAgentKit: "AgentKit is not enabled",
97
+ piProfileNotSafe: "profile is unmanaged, unhealthy, or drifted",
98
+ piProfileDiscoveryUnavailable: "Pi profile discovery is unavailable: {message}",
87
99
  projectCandidate: "{name} — {kit} ({runtime}) — {path}",
88
100
  selectUpdateAll: "Select what to update (Space toggles, Enter confirms)",
89
101
  updateEverything: "Update all discovered Kit installs: global + projects ({count})",
90
102
  updateAllProjects: "Update projects only ({count})",
91
103
  chooseUpdates: "Choose individual targets…",
92
- backFromTargetSelection: " Back (select with Space, then confirm with Enter)",
104
+ backFromTargetSelection: "Esc: Back",
93
105
  confirmSelectedUpdates: "What should happen with the {count} selected target(s)?",
94
106
  previewSelectedUpdates: "Continue to preview ({count})",
95
107
  noProjectsInRegistry: "No updateable registered projects were found.",
@@ -128,6 +140,12 @@ const messages = {
128
140
  languagePrompt: "Chọn ngôn ngữ giao diện / Choose interface language",
129
141
  commandPrompt: "Bạn muốn làm gì?",
130
142
  currentBinary: "ak binary hiện tại: {version} ({channel})",
143
+ checkingBinaryUpdate: "Đang kiểm tra bản cập nhật ak binary…",
144
+ checkedBinaryUpdate: "Đã kiểm tra cập nhật ak binary.",
145
+ binaryUpdateAvailable: "CÓ BẢN MỚI: ak {current} → {latest} ({channel}). Helper đã focus sẵn action cập nhật.",
146
+ binaryUpToDate: "ĐÃ LÀ BẢN MỚI NHẤT: ak {version} trên channel {channel}.",
147
+ binaryUpdateStatusUnknown: "Không thể xác định trạng thái cập nhật binary (status: {status}).",
148
+ binaryUpdateCheckFailed: "Không thể kiểm tra cập nhật ak binary: {message}. Helper vẫn tiếp tục hoạt động.",
131
149
  installAction: "Cài Kit",
132
150
  updateAction: "Cập nhật Kit",
133
151
  selfUpdateAction: "Cập nhật ak binary",
@@ -135,6 +153,7 @@ const messages = {
135
153
  exportAction: "Export Kit (không cài vào runtime)",
136
154
  doctorAction: "Chạy health check",
137
155
  backToLanguage: "← Quay lại chọn ngôn ngữ",
156
+ escapeBack: "Esc: Quay lại",
138
157
  chooseProject: "Chọn thư mục project",
139
158
  currentProjectScope: "Dùng project hiện tại ({path})",
140
159
  globalScope: "Dùng Kit ở scope user/global",
@@ -207,12 +226,17 @@ const messages = {
207
226
  unregisteredProjects: "Project khác đã discover:",
208
227
  otherInstalls: "Global scope:",
209
228
  globalCandidate: "{kit} global scope ({runtimes})",
229
+ piProfileCandidate: "Engineer Kit — profile {runtime}: {profile}",
230
+ piProfileSkipped: "Pi Profile Manager bỏ qua {profile}: {reason}",
231
+ piProfileNoAgentKit: "chưa bật AgentKit",
232
+ piProfileNotSafe: "profile không được quản lý, không healthy hoặc đã drift",
233
+ piProfileDiscoveryUnavailable: "Không thể discover Pi profile: {message}",
210
234
  projectCandidate: "{name} — {kit} ({runtime}) — {path}",
211
235
  selectUpdateAll: "Chọn mục cần update (Space bật/tắt, Enter xác nhận)",
212
236
  updateEverything: "Update mọi Kit đã detect: global + project ({count})",
213
237
  updateAllProjects: "Chỉ update các project đã discover ({count})",
214
238
  chooseUpdates: "Chọn từng target…",
215
- backFromTargetSelection: " Quay lại (nhấn Space rồi Enter)",
239
+ backFromTargetSelection: "Esc: Quay lại",
216
240
  confirmSelectedUpdates: "Xử lý thế nào với {count} target đã chọn?",
217
241
  previewSelectedUpdates: "Tiếp tục preview ({count})",
218
242
  noProjectsInRegistry: "Không tìm thấy registered project nào có thể update.",
@@ -0,0 +1,102 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { extname, isAbsolute, resolve, win32 } from "node:path";
4
+
5
+ function validPath(value) {
6
+ return typeof value === "string" && (isAbsolute(value) || win32.isAbsolute(value));
7
+ }
8
+
9
+ export function piProfileManagerInvocation(
10
+ configuredBinary = "pi-profile-manager",
11
+ {
12
+ platform = process.platform,
13
+ home = homedir(),
14
+ fileExists = existsSync,
15
+ nodeBinary = process.execPath,
16
+ } = {},
17
+ ) {
18
+ if (platform !== "win32") return { binary: configuredBinary, prefixArgs: [] };
19
+
20
+ const extension = extname(configuredBinary).toLowerCase();
21
+ if ([".js", ".cjs", ".mjs"].includes(extension)) {
22
+ return { binary: nodeBinary, prefixArgs: [configuredBinary] };
23
+ }
24
+
25
+ const siblingModule = extension === ".cmd"
26
+ ? configuredBinary.slice(0, -extension.length) + ".mjs"
27
+ : null;
28
+ const managedModule = (platform === "win32" ? win32.resolve : resolve)(
29
+ home,
30
+ "bin",
31
+ "pi-profile-manager.mjs",
32
+ );
33
+ const modulePath = siblingModule && fileExists(siblingModule)
34
+ ? siblingModule
35
+ : (!configuredBinary.includes("/") && !configuredBinary.includes("\\") && fileExists(managedModule)
36
+ ? managedModule
37
+ : null);
38
+ return modulePath
39
+ ? { binary: nodeBinary, prefixArgs: [modulePath] }
40
+ : { binary: configuredBinary, prefixArgs: [] };
41
+ }
42
+
43
+ export function parsePiProfileInventory(output) {
44
+ const value = JSON.parse(output);
45
+ if (value?.schemaVersion !== 1 || !Array.isArray(value.profiles)) {
46
+ throw new Error("unsupported pi-profile-manager profile inventory contract");
47
+ }
48
+ const ids = new Set();
49
+ return value.profiles.map((profile) => {
50
+ const sessionIsValid = profile?.runtime === "omp"
51
+ ? profile.sessionDir === null
52
+ : validPath(profile?.sessionDir);
53
+ if (
54
+ !profile || typeof profile.id !== "string" || !profile.id.trim() ||
55
+ !["pi", "omp"].includes(profile.runtime) || !validPath(profile.agentDir) ||
56
+ !sessionIsValid || typeof profile.agentkitEnabled !== "boolean" ||
57
+ typeof profile.managed !== "boolean" || typeof profile.healthy !== "boolean"
58
+ ) {
59
+ throw new Error("invalid pi-profile-manager profile inventory entry");
60
+ }
61
+ if (ids.has(profile.id)) {
62
+ throw new Error("duplicate pi-profile-manager profile inventory entry");
63
+ }
64
+ ids.add(profile.id);
65
+ return {
66
+ id: profile.id,
67
+ runtime: profile.runtime,
68
+ agentDir: profile.agentDir,
69
+ sessionDir: profile.sessionDir,
70
+ agentkitEnabled: profile.agentkitEnabled,
71
+ managed: profile.managed,
72
+ healthy: profile.healthy,
73
+ };
74
+ });
75
+ }
76
+
77
+ export function piProfileProcessOptions(profile) {
78
+ if (profile.runtime === "omp") {
79
+ return {
80
+ envOverrides: { AGENTKIT_OMP_HOME: profile.agentDir },
81
+ envUnset: ["PI_CODING_AGENT_DIR", "PI_CODING_AGENT_SESSION_DIR", "OMP_HOME"],
82
+ };
83
+ }
84
+ return {
85
+ envOverrides: {
86
+ PI_CODING_AGENT_DIR: profile.agentDir,
87
+ PI_CODING_AGENT_SESSION_DIR: profile.sessionDir,
88
+ },
89
+ envUnset: ["AGENTKIT_OMP_HOME", "OMP_HOME"],
90
+ };
91
+ }
92
+
93
+ export function classifyPiProfiles(profiles) {
94
+ return {
95
+ updateable: profiles.filter((profile) => (
96
+ profile.agentkitEnabled && profile.managed && profile.healthy
97
+ )),
98
+ skipped: profiles.filter((profile) => !(
99
+ profile.agentkitEnabled && profile.managed && profile.healthy
100
+ )),
101
+ };
102
+ }
package/lib/prompts.mjs CHANGED
@@ -60,11 +60,31 @@ export async function choose(message, choices, defaultIndex = 0) {
60
60
  }));
61
61
  }
62
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);
63
+ async function escapeBack(prompt) {
64
+ let cancelKey = null;
65
+ const trackCancelKey = (chunk) => {
66
+ const input = String(chunk);
67
+ if (input === "\u001b") cancelKey = "escape";
68
+ if (input === "\u0003") cancelKey = "interrupt";
69
+ };
70
+ stdin.on("data", trackCancelKey);
71
+ try {
72
+ const value = await prompt();
73
+ if (!isCancel(value)) return value;
74
+ if (cancelKey === "escape") return BACK;
75
+ return unwrap(value);
76
+ } finally {
77
+ stdin.off("data", trackCancelKey);
78
+ }
79
+ }
80
+
81
+ export async function chooseWithBack(message, choices, defaultIndex = 0, backLabel = "Esc: Back") {
82
+ startSession();
83
+ return escapeBack(() => select({
84
+ message: highlightPrompt(`${message} · ${backLabel}`),
85
+ options: choices,
86
+ initialValue: choices[defaultIndex]?.value,
87
+ }));
68
88
  }
69
89
 
70
90
  export async function multiChoose(message, choices, initialValues = choices.map((choice) => choice.value)) {
@@ -81,13 +101,15 @@ export async function multiChooseWithBack(
81
101
  message,
82
102
  choices,
83
103
  initialValues = choices.map((choice) => choice.value),
84
- backLabel = " Back (select with Space, then confirm with Enter)",
104
+ backLabel = "Esc: Back",
85
105
  ) {
86
- const values = await multiChoose(message, [
87
- ...choices,
88
- { label: backLabel, value: BACK },
89
- ], initialValues);
90
- return values.includes(BACK) ? BACK : values;
106
+ startSession();
107
+ return escapeBack(() => multiselect({
108
+ message: highlightPrompt(`${message} · ${backLabel}`),
109
+ options: choices,
110
+ initialValues,
111
+ required: true,
112
+ }));
91
113
  }
92
114
 
93
115
  export async function ask(message) {
package/lib/runner.mjs CHANGED
@@ -1,4 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { extname } from "node:path";
3
+
4
+ export function spawnInvocation(binary, args, platform = process.platform) {
5
+ if (platform === "win32" && [".js", ".cjs", ".mjs"].includes(extname(binary).toLowerCase())) {
6
+ return { binary: process.execPath, args: [binary, ...args] };
7
+ }
8
+ return { binary, args };
9
+ }
2
10
 
3
11
  function jsonErrorMessage(output) {
4
12
  const candidates = [output, ...String(output).split("\n").reverse()];
@@ -56,13 +64,24 @@ export function requiresForceConsent(error) {
56
64
  /--force/i.test(diagnostic);
57
65
  }
58
66
 
59
- export function run(binary, args, { cwd = process.cwd(), stdio = "inherit" } = {}) {
67
+ function childEnv(envOverrides = {}, envUnset = []) {
68
+ const env = { ...process.env, ...envOverrides };
69
+ for (const name of envUnset) delete env[name];
70
+ return env;
71
+ }
72
+
73
+ export function run(
74
+ binary,
75
+ args,
76
+ { cwd = process.cwd(), stdio = "inherit", envOverrides = {}, envUnset = [] } = {},
77
+ ) {
60
78
  return new Promise((resolve, reject) => {
61
- const child = spawn(binary, args, {
79
+ const invocation = spawnInvocation(binary, args);
80
+ const child = spawn(invocation.binary, invocation.args, {
62
81
  cwd,
63
82
  stdio,
64
83
  shell: false,
65
- env: process.env,
84
+ env: childEnv(envOverrides, envUnset),
66
85
  });
67
86
  child.once("error", (error) => {
68
87
  error.command = { binary, args: [...args], cwd };
@@ -87,13 +106,18 @@ export function run(binary, args, { cwd = process.cwd(), stdio = "inherit" } = {
87
106
  });
88
107
  }
89
108
 
90
- export function runCapture(binary, args, { cwd = process.cwd() } = {}) {
109
+ export function runCapture(
110
+ binary,
111
+ args,
112
+ { cwd = process.cwd(), envOverrides = {}, envUnset = [] } = {},
113
+ ) {
91
114
  return new Promise((resolve, reject) => {
92
- const child = spawn(binary, args, {
115
+ const invocation = spawnInvocation(binary, args);
116
+ const child = spawn(invocation.binary, invocation.args, {
93
117
  cwd,
94
118
  stdio: ["ignore", "pipe", "pipe"],
95
119
  shell: false,
96
- env: process.env,
120
+ env: childEnv(envOverrides, envUnset),
97
121
  });
98
122
  let stdout = "";
99
123
  let stderr = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thieung/agentkit-helper",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Community helper for installing and updating AgentKit kits by runtime and channel",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "bin/",
12
12
  "lib/",
13
+ "scripts/",
13
14
  "assets/",
14
15
  "README.md",
15
16
  "README.vi.md",
@@ -17,7 +18,7 @@
17
18
  ],
18
19
  "scripts": {
19
20
  "test": "node --test --test-concurrency=1 test/*.test.mjs",
20
- "check": "node --check bin/agentkit-helper.mjs && node --check lib/*.mjs"
21
+ "check": "node scripts/check.mjs"
21
22
  },
22
23
  "engines": {
23
24
  "node": ">=20.12"
@@ -0,0 +1,16 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readdirSync } from "node:fs";
3
+
4
+ const files = [
5
+ "bin/agentkit-helper.mjs",
6
+ ...readdirSync("lib")
7
+ .filter((name) => name.endsWith(".mjs"))
8
+ .sort()
9
+ .map((name) => `lib/${name}`),
10
+ ];
11
+
12
+ for (const file of files) {
13
+ const result = spawnSync(process.execPath, ["--check", file], { stdio: "inherit" });
14
+ if (result.error) throw result.error;
15
+ if (result.status !== 0) process.exit(result.status ?? 1);
16
+ }