abelworkflow 1.1.1 → 1.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/lib/cli/main.mjs CHANGED
@@ -126,31 +126,32 @@ ${c.bold("Default behavior:")}
126
126
  `);
127
127
  }
128
128
 
129
- async function configureCodexForPaths(paths) {
129
+ async function configureProviderWithMetadata(paths, configure) {
130
130
  const metadata = await readInstallMetadata(paths);
131
131
  const packageVersion = await packageVersionFor(paths, metadata);
132
- const result = await configureCodexApi(paths, promptApi, {
133
- managedAuthKeys: getPreviousManagedCodexAuthKeys(metadata),
134
- managedCodexAgentFiles: getPreviousManagedCodexAgentFiles(metadata)
135
- });
132
+ const overrides = await configure(metadata);
136
133
  await writeInstallMetadata(paths, finalizeProviderInstallMetadata({
137
134
  previousMetadata: metadata,
138
135
  packageVersion,
139
- overrides: {
136
+ overrides
137
+ }));
138
+ }
139
+
140
+ async function configureCodexForPaths(paths) {
141
+ await configureProviderWithMetadata(paths, async (metadata) => {
142
+ const result = await configureCodexApi(paths, promptApi, {
143
+ managedAuthKeys: getPreviousManagedCodexAuthKeys(metadata),
144
+ managedCodexAgentFiles: getPreviousManagedCodexAgentFiles(metadata)
145
+ });
146
+ return {
140
147
  managedCodexAuthKeys: result.managedAuthKeys,
141
148
  managedCodexAgentFiles: result.managedCodexAgentFiles
142
- }
143
- }));
149
+ };
150
+ });
144
151
  }
145
152
 
146
153
  async function configureClaudeForPaths(paths) {
147
- const metadata = await readInstallMetadata(paths);
148
- const packageVersion = await packageVersionFor(paths, metadata);
149
- await configureClaudeApi(paths, promptApi);
150
- await writeInstallMetadata(paths, finalizeProviderInstallMetadata({
151
- previousMetadata: metadata,
152
- packageVersion
153
- }));
154
+ await configureProviderWithMetadata(paths, () => configureClaudeApi(paths, promptApi));
154
155
  }
155
156
 
156
157
  async function runFullInit(options) {
@@ -176,13 +177,13 @@ async function runFullInit(options) {
176
177
  await installCliTool("pi", promptApi);
177
178
  }
178
179
  if (await confirmOrCancel({ message: "是否配置 Pi 当前有效 Provider API?", initialValue: commandExists("pi") })) {
179
- await configurePiApi(options.paths, ensurePiResourcesLinked, promptApi);
180
+ await configurePiApi(options.paths, ensurePiResourcesLinkedWithReport, promptApi);
180
181
  }
181
182
  if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
182
- await configureGrokSearchEnv(options.paths, (paths) => ensureSkillPresent(paths, "grok-search"), promptApi);
183
+ await configureGrokSearchEnv(options.paths, (paths) => ensureSkillPresentWithReport(paths, "grok-search"), promptApi);
183
184
  }
184
185
  if (await confirmOrCancel({ message: "是否填写 context7-auto-research 环境变量?", initialValue: false })) {
185
- await configureContext7Env(options.paths, (paths) => ensureSkillPresent(paths, "context7-auto-research"), promptApi);
186
+ await configureContext7Env(options.paths, (paths) => ensureSkillPresentWithReport(paths, "context7-auto-research"), promptApi);
186
187
  }
187
188
 
188
189
  const conflictCount = uniqueReportPaths(installReport.conflicts).length;
@@ -205,35 +206,31 @@ async function runInteractiveMenu(options) {
205
206
  return opt;
206
207
  };
207
208
  const cliToolMenus = {
208
- "pi-cli": {
209
- tool: "pi",
209
+ pi: {
210
210
  title: "Pi",
211
- actions: {
212
- "pi-install": async () => installCliTool("pi", promptApi),
213
- "pi-api": async () => configurePiApi(options.paths, ensurePiResourcesLinkedWithReport, promptApi)
214
- }
211
+ install: () => installCliTool("pi", promptApi),
212
+ configure: () => configurePiApi(options.paths, ensurePiResourcesLinkedWithReport, promptApi)
215
213
  },
216
- "codex-cli": {
217
- tool: "codex",
214
+ codex: {
218
215
  title: "Codex",
219
- actions: {
220
- "codex-install": async () => installCliTool("codex", promptApi),
221
- "codex-api": async () => configureCodexForPaths(options.paths)
222
- }
216
+ install: () => installCliTool("codex", promptApi),
217
+ configure: () => configureCodexForPaths(options.paths)
223
218
  },
224
- "claude-cli": {
225
- tool: "claude",
219
+ claude: {
226
220
  title: "Claude Code",
227
- actions: {
228
- "claude-install": async () => installCliTool("claude", promptApi),
229
- "claude-api": async () => configureClaudeForPaths(options.paths)
230
- }
221
+ install: () => installCliTool("claude", promptApi),
222
+ configure: () => configureClaudeForPaths(options.paths)
231
223
  }
232
224
  };
233
- const runCliToolMenu = async ({ tool, title, actions }) => {
225
+ const runCliToolMenu = async (tool) => {
226
+ const menu = cliToolMenus[tool];
227
+ const actions = {
228
+ [`${tool}-api`]: menu.configure,
229
+ [`${tool}-install`]: menu.install
230
+ };
234
231
  while (true) {
235
232
  const choice = await p.select({
236
- message: `请选择 ${title} 操作`,
233
+ message: `请选择 ${menu.title} 操作`,
237
234
  options: buildCliToolMenuDescriptors(tool).map(buildOption),
238
235
  initialValue: `${tool}-api`
239
236
  });
@@ -259,9 +256,9 @@ async function runInteractiveMenu(options) {
259
256
  }),
260
257
  "grok-search": async () => configureGrokSearchEnv(options.paths, (paths) => ensureSkillPresentWithReport(paths, "grok-search"), promptApi),
261
258
  context7: async () => configureContext7Env(options.paths, (paths) => ensureSkillPresentWithReport(paths, "context7-auto-research"), promptApi),
262
- "pi-cli": async () => runCliToolMenu(cliToolMenus["pi-cli"]),
263
- "codex-cli": async () => runCliToolMenu(cliToolMenus["codex-cli"]),
264
- "claude-cli": async () => runCliToolMenu(cliToolMenus["claude-cli"])
259
+ "pi-cli": async () => runCliToolMenu("pi"),
260
+ "codex-cli": async () => runCliToolMenu("codex"),
261
+ "claude-cli": async () => runCliToolMenu("claude")
265
262
  };
266
263
 
267
264
  while (true) {
@@ -376,10 +373,4 @@ async function main(argv, runtime = {}) {
376
373
  }
377
374
  }
378
375
 
379
- export {
380
- main,
381
- presentInstallReport,
382
- printHelp,
383
- runFullInit,
384
- runInteractiveMenu
385
- };
376
+ export { main, presentInstallReport };
@@ -11,22 +11,10 @@ const interactiveMenuDescriptors = [
11
11
  { value: "exit", label: "退出", group: "exit" }
12
12
  ];
13
13
  const interactiveMenuDefaultValue = "full-init";
14
- const cliToolMenuDescriptorMap = {
15
- pi: [
16
- { value: "pi-api", label: "配置 Pi API" },
17
- { value: "pi-install", label: "安装/更新 Pi" },
18
- { value: "back", label: "返回上一级" }
19
- ],
20
- codex: [
21
- { value: "codex-api", label: "配置 Codex API" },
22
- { value: "codex-install", label: "安装/更新 Codex" },
23
- { value: "back", label: "返回上一级" }
24
- ],
25
- claude: [
26
- { value: "claude-api", label: "配置 Claude Code API" },
27
- { value: "claude-install", label: "安装/更新 Claude Code" },
28
- { value: "back", label: "返回上一级" }
29
- ]
14
+ const cliToolLabels = {
15
+ pi: "Pi",
16
+ codex: "Codex",
17
+ claude: "Claude Code"
30
18
  };
31
19
  class CancelledError extends Error {
32
20
  constructor(message = "用户取消") {
@@ -36,9 +24,13 @@ class CancelledError extends Error {
36
24
  }
37
25
 
38
26
  function buildCliToolMenuDescriptors(tool) {
39
- const descriptors = cliToolMenuDescriptorMap[tool];
40
- if (!descriptors) throw new Error(`Unknown CLI tool: ${tool}`);
41
- return descriptors.map((descriptor) => ({ ...descriptor }));
27
+ const label = cliToolLabels[tool];
28
+ if (!label) throw new Error(`Unknown CLI tool: ${tool}`);
29
+ return [
30
+ { value: `${tool}-api`, label: `配置 ${label} API` },
31
+ { value: `${tool}-install`, label: `安装/更新 ${label}` },
32
+ { value: "back", label: "返回上一级" }
33
+ ];
42
34
  }
43
35
 
44
36
  function required(message = "此项不能为空") {
@@ -79,6 +71,16 @@ function assertNotCancelled(value) {
79
71
  if (p.isCancel(value)) throw new CancelledError();
80
72
  }
81
73
 
74
+ async function passwordOrExisting({ message, existingValue, requiredMessage = "API Key 不能为空" }) {
75
+ const input = await p.password(passwordPromptOptions(
76
+ message,
77
+ existingValue,
78
+ requiredMessage ? requiredUnlessExisting(existingValue, requiredMessage) : undefined
79
+ ));
80
+ assertNotCancelled(input);
81
+ return resolvePasswordValue(input, existingValue);
82
+ }
83
+
82
84
  async function confirmOrCancel({ message, initialValue = false }) {
83
85
  const value = await p.confirm({ message, initialValue, active: "是", inactive: "否" });
84
86
  assertNotCancelled(value);
@@ -98,6 +100,7 @@ export {
98
100
  confirmOrCancel,
99
101
  interactiveMenuDefaultValue,
100
102
  interactiveMenuDescriptors,
103
+ passwordOrExisting,
101
104
  passwordPromptOptions,
102
105
  required,
103
106
  requiredUnlessExisting,
@@ -34,14 +34,6 @@ function quoteEnvValue(value) {
34
34
  throw new Error("Environment value cannot be represented without changing its meaning");
35
35
  }
36
36
 
37
- function renderDotenv(values) {
38
- const lines = Object.entries(values)
39
- .filter(([, value]) => value !== undefined && value !== null && value !== "")
40
- .sort(([left], [right]) => left.localeCompare(right))
41
- .map(([key, value]) => `${key}=${quoteEnvValue(String(value))}`);
42
- return lines.length ? `${lines.join("\n")}\n` : "";
43
- }
44
-
45
37
  function getDotenvLines(content) {
46
38
  const lines = [];
47
39
  for (let start = 0; start < content.length;) {
@@ -99,4 +91,4 @@ function updateDotenvContent(content, updates) {
99
91
  return next;
100
92
  }
101
93
 
102
- export { parseDotenv, quoteEnvValue, renderDotenv, updateDotenvContent };
94
+ export { parseDotenv, quoteEnvValue, updateDotenvContent };
@@ -6,7 +6,6 @@ import { stripJsonComments } from "./jsonc.mjs";
6
6
 
7
7
  const newBackupMarker = ".abelworkflow.bak.";
8
8
  const newBackupSuffixPattern = /^\d+-\d+-\d{10,}$/u;
9
- const chmod = fs.chmod;
10
9
  let uniqueFileIndex = 0;
11
10
 
12
11
  function nextUniqueSuffix() {
@@ -225,21 +224,6 @@ async function updateLockedJson(path, updater, options = {}) {
225
224
  }
226
225
  }
227
226
 
228
- async function backupExistingPath(targetPath, options = {}) {
229
- return copyBackup(targetPath, options);
230
- }
231
-
232
- async function backupPrivateFile(targetPath, content, options = {}) {
233
- if (!(await pathExists(targetPath))) return null;
234
- const backupLimit = options.backupLimit ?? 3;
235
- if (backupLimit === 0) return null;
236
- const backupPath = await createBackupPath(targetPath);
237
- await fs.writeFile(backupPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
238
- if (isPosix()) await fs.chmod(backupPath, 0o600);
239
- await pruneNewBackups(targetPath, backupLimit);
240
- return backupPath;
241
- }
242
-
243
227
  async function backupIfNeeded(targetPath, options = {}) {
244
228
  if (!(await pathExists(targetPath))) return null;
245
229
  const backupLimit = options.backupLimit ?? 3;
@@ -281,19 +265,8 @@ async function updateDotenvFile(path, updates, options = {}) {
281
265
  return writeText(path, updateDotenvContent(content, updates), options);
282
266
  }
283
267
 
284
- async function writeJsonFileSafe(path, data, options = {}) {
285
- return writeJson(path, data, { ...options, backupLimit: options.backupLimit ?? 0 });
286
- }
287
-
288
- async function writeJsonFileWithBackup(path, data, options = {}) {
289
- return writeJson(path, data, options);
290
- }
291
-
292
268
  export {
293
- backupExistingPath,
294
269
  backupIfNeeded,
295
- backupPrivateFile,
296
- chmod,
297
270
  ensurePrivateJsonFile,
298
271
  pathExists,
299
272
  pathTargetExists,
@@ -304,7 +277,5 @@ export {
304
277
  updateDotenvFile,
305
278
  updateLockedJson,
306
279
  writeJson,
307
- writeJsonFileSafe,
308
- writeJsonFileWithBackup,
309
280
  writeText
310
281
  };
@@ -46,6 +46,15 @@ function removeTomlSection(content, sectionName) {
46
46
  return `${content.slice(0, section.start)}${content.slice(section.end)}`;
47
47
  }
48
48
 
49
+ function removeTomlSectionField(content, sectionName, field) {
50
+ const document = parseTomlDocument(content);
51
+ const section = findTomlSection(document, sectionName);
52
+ const entry = section && findTomlAssignment(document.assignments, field, section);
53
+ return entry
54
+ ? `${content.slice(0, entry.start)}${content.slice(entry.lineEnd)}`
55
+ : content;
56
+ }
57
+
49
58
  function buildTomlSection(sectionName, values, lineEnding = "\n") {
50
59
  const lines = [`[${sectionName}]`];
51
60
  for (const [key, value] of Object.entries(values)) {
@@ -399,14 +408,6 @@ function findTomlAssignment(assignments, field, section) {
399
408
  ));
400
409
  }
401
410
 
402
- function splitTopLevelTomlContent(content) {
403
- const { topLevelEnd } = parseTomlDocument(content);
404
- return {
405
- topLevel: topLevelEnd === -1 ? content : content.slice(0, topLevelEnd),
406
- rest: topLevelEnd === -1 ? "" : content.slice(topLevelEnd)
407
- };
408
- }
409
-
410
411
  function extractTopLevelTomlEntries(content) {
411
412
  const document = parseTomlDocument(content);
412
413
  return document.assignments
@@ -631,8 +632,8 @@ export {
631
632
  parseTomlSection,
632
633
  readTopLevelTomlString,
633
634
  removeTomlSection,
635
+ removeTomlSectionField,
634
636
  removeTopLevelTomlField,
635
- splitTopLevelTomlContent,
636
637
  updateTomlSectionFields,
637
638
  updateTopLevelTomlField
638
639
  };
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import * as fs from "node:fs/promises";
3
2
  import { join, relative, resolve } from "node:path";
4
3
  import {
@@ -8,6 +7,7 @@ import {
8
7
  readJsonFileSafe,
9
8
  writeText
10
9
  } from "../config/store.mjs";
10
+ import { hashBytes } from "../utils.mjs";
11
11
  import { createInstallReport, readInstallMetadata } from "./state.mjs";
12
12
 
13
13
  const ignoredSkillPathPatterns = [
@@ -69,10 +69,6 @@ function mapGitignoreTemplate(relativePath) {
69
69
  return relativePath.replace(/(^|\/)gitignore\.template$/u, "$1.gitignore");
70
70
  }
71
71
 
72
- function hashBytes(content) {
73
- return createHash("sha256").update(content).digest("hex");
74
- }
75
-
76
72
  function shouldCopySkillPath(skillsRoot, sourcePath) {
77
73
  const relativePath = normalizeRelativePath(relative(skillsRoot, sourcePath));
78
74
  if (!relativePath) return true;
@@ -316,9 +312,9 @@ async function ensureManagedContainerDirectory(targetPath, sourcePath) {
316
312
  export {
317
313
  collectManagedAssets,
318
314
  ensureManagedContainerDirectory,
319
- hashBytes,
320
315
  pathsReferToSameEntry,
321
316
  readManagedFiles,
317
+ readPackageVersion,
322
318
  shouldCopySkillPath,
323
319
  syncManagedFiles
324
320
  };
@@ -1,7 +1,6 @@
1
- import { join } from "node:path";
2
- import { pathExists, readJsonFileSafe } from "../config/store.mjs";
3
- import { createPaths, defaultPaths } from "../paths.mjs";
4
- import { readManagedFiles, syncManagedFiles } from "./assets.mjs";
1
+ import { pathExists } from "../config/store.mjs";
2
+ import { createPaths } from "../paths.mjs";
3
+ import { readManagedFiles, readPackageVersion, syncManagedFiles } from "./assets.mjs";
5
4
  import { linkClaude, linkCodex, linkPi } from "./links.mjs";
6
5
  import {
7
6
  buildInstallMetadata,
@@ -27,15 +26,6 @@ function validatedPaths(paths) {
27
26
  });
28
27
  }
29
28
 
30
- function resolveInstallPaths(options = {}) {
31
- if (options.paths) return validatedPaths(options.paths);
32
- return createPaths({
33
- homeDir: options.homeDir ?? defaultPaths.homeDir,
34
- packageRoot: options.packageRoot ?? defaultPaths.packageRoot,
35
- agentsDir: options.agentsDir ?? defaultPaths.agentsDir
36
- });
37
- }
38
-
39
29
  function reportFromLinkResults(results) {
40
30
  const report = createInstallReport();
41
31
  for (const result of results) {
@@ -58,12 +48,17 @@ async function packageVersionFor(paths, previousMetadata) {
58
48
  && previousMetadata.packageVersion) {
59
49
  return previousMetadata.packageVersion;
60
50
  }
61
- const packagePath = join(paths.packageRoot, "package.json");
62
- const packageData = await readJsonFileSafe(packagePath);
63
- if (typeof packageData.version !== "string" || !packageData.version) {
64
- throw new Error(`Missing package version in ${packagePath}`);
65
- }
66
- return packageData.version;
51
+ return readPackageVersion(paths);
52
+ }
53
+
54
+ async function writeInstallState(paths, assetResult, linkedTargets) {
55
+ const metadata = buildInstallMetadata({
56
+ previousMetadata: assetResult.previousMetadata,
57
+ packageVersion: assetResult.packageVersion,
58
+ managedFiles: assetResult.managedFiles,
59
+ linkedTargets
60
+ });
61
+ await writeInstallMetadata(paths, metadata);
67
62
  }
68
63
 
69
64
  async function installWorkflow(options) {
@@ -94,20 +89,12 @@ async function installWorkflow(options) {
94
89
  const codexResults = await linkCodex(paths, previousLinkedTargets, linkOptions);
95
90
  const piResults = await linkPi(paths, previousLinkedTargets, linkOptions);
96
91
  const linkResults = [...claudeResults, ...codexResults, ...piResults];
97
- const linkedTargets = mergeLinkedTargets(previousLinkedTargets, linkResults);
98
-
99
- const metadata = buildInstallMetadata({
100
- previousMetadata,
101
- packageVersion: assetResult.packageVersion,
102
- managedFiles: assetResult.managedFiles,
103
- linkedTargets
104
- });
105
- await writeInstallMetadata(paths, metadata);
92
+ await writeInstallState(paths, assetResult, mergeLinkedTargets(previousLinkedTargets, linkResults));
106
93
  return mergeInstallReports(assetResult.report, reportFromLinkResults(linkResults));
107
94
  }
108
95
 
109
96
  async function installManagedWorkflow(options = {}) {
110
- return installWorkflow({ ...options, paths: resolveInstallPaths(options) });
97
+ return installWorkflow({ ...options, paths: validatedPaths(options.paths) });
111
98
  }
112
99
 
113
100
  async function ensureSkillPresent(inputPaths, skillName) {
@@ -118,13 +105,7 @@ async function ensureSkillPresent(inputPaths, skillName) {
118
105
  force: false,
119
106
  pathPrefix: `skills/${skillName}/`
120
107
  });
121
- const metadata = buildInstallMetadata({
122
- previousMetadata,
123
- packageVersion: assetResult.packageVersion,
124
- managedFiles: assetResult.managedFiles,
125
- linkedTargets: previousMetadata.linkedTargets ?? {}
126
- });
127
- await writeInstallMetadata(paths, metadata);
108
+ await writeInstallState(paths, assetResult, previousMetadata.linkedTargets ?? {});
128
109
  return assetResult.report;
129
110
  }
130
111
 
@@ -134,13 +115,7 @@ async function ensurePiResourcesLinked(inputPaths) {
134
115
  const assetResult = await syncManagedFiles({ paths, force: false });
135
116
  const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
136
117
  const piResults = await linkPi(paths, previousLinkedTargets, { force: false });
137
- const metadata = buildInstallMetadata({
138
- previousMetadata,
139
- packageVersion: assetResult.packageVersion,
140
- managedFiles: assetResult.managedFiles,
141
- linkedTargets: mergeLinkedTargets(previousLinkedTargets, piResults)
142
- });
143
- await writeInstallMetadata(paths, metadata);
118
+ await writeInstallState(paths, assetResult, mergeLinkedTargets(previousLinkedTargets, piResults));
144
119
  return mergeInstallReports(assetResult.report, reportFromLinkResults(piResults));
145
120
  }
146
121
 
@@ -1,12 +1,9 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { backupIfNeeded, pathExists, pathTargetExists } from "../config/store.mjs";
4
- import { isWindows } from "../paths.mjs";
5
- import { ensureManagedContainerDirectory, hashBytes } from "./assets.mjs";
6
-
7
- function shouldForceFileSymlinkFailure(kind) {
8
- return process.env.ABELWORKFLOW_TEST_FORCE_FILE_SYMLINK_EPERM === "1" && isWindows() && kind === "file";
9
- }
4
+ import { containsPath, isWindows } from "../paths.mjs";
5
+ import { hashBytes } from "../utils.mjs";
6
+ import { ensureManagedContainerDirectory } from "./assets.mjs";
10
7
 
11
8
  async function createManagedTargetState(targetPath, sourcePath, kind, mode, status) {
12
9
  let targetHash;
@@ -16,12 +13,7 @@ async function createManagedTargetState(targetPath, sourcePath, kind, mode, stat
16
13
  return { targetPath, sourcePath, kind, mode, status, ...(targetHash ? { targetHash } : {}) };
17
14
  }
18
15
 
19
- async function createSymlink(targetPath, sourcePath, linkType, kind) {
20
- if (shouldForceFileSymlinkFailure(kind)) {
21
- const error = new Error("simulated EPERM");
22
- error.code = "EPERM";
23
- throw error;
24
- }
16
+ async function createSymlink(targetPath, sourcePath, linkType) {
25
17
  await fs.symlink(sourcePath, targetPath, linkType);
26
18
  }
27
19
 
@@ -150,7 +142,7 @@ async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTar
150
142
 
151
143
  const linkType = isWindows() ? (kind === "dir" ? "junction" : "file") : kind;
152
144
  try {
153
- await createSymlink(targetPath, sourcePath, linkType, kind);
145
+ await createSymlink(targetPath, sourcePath, linkType);
154
146
  return createManagedTargetState(targetPath, sourcePath, kind, "symlink", "linked");
155
147
  } catch (error) {
156
148
  if (!shouldFallbackToManagedFile(error, kind)) throw error;
@@ -258,11 +250,8 @@ async function getPiExtensionNames(extensionsDir) {
258
250
  }
259
251
 
260
252
  function isWithinManagedRoot(targetPath, managedSourceRoot, pathOps = { isAbsolute, relative, sep }) {
261
- const relativePath = pathOps.relative(managedSourceRoot, targetPath);
262
- return relativePath !== ""
263
- && !pathOps.isAbsolute(relativePath)
264
- && relativePath !== ".."
265
- && !relativePath.startsWith(`..${pathOps.sep}`);
253
+ return pathOps.relative(managedSourceRoot, targetPath) !== ""
254
+ && containsPath(managedSourceRoot, targetPath, pathOps);
266
255
  }
267
256
 
268
257
  async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames, previousLinkedTargets, options = {}) {
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { readJsonFileSafe, writeJson } from "../config/store.mjs";
3
+ import { isManagedCodexAgentFileEntry } from "../utils.mjs";
3
4
 
4
5
  function metadataPathFor(paths) {
5
6
  return join(paths.agentsDir, paths.installMetadataName);
@@ -30,11 +31,7 @@ function getPreviousManagedCodexAuthKeys(previousMetadata = {}) {
30
31
  function normalizeManagedCodexAgentFiles(value = {}) {
31
32
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
32
33
  return sortObject(Object.fromEntries(Object.entries(value).filter(([name, hash]) => (
33
- name.endsWith(".toml")
34
- && !name.includes("/")
35
- && !name.includes("\\")
36
- && typeof hash === "string"
37
- && /^[a-f0-9]{64}$/u.test(hash)
34
+ isManagedCodexAgentFileEntry(name, hash)
38
35
  ))));
39
36
  }
40
37
 
@@ -63,7 +60,7 @@ function buildInstallMetadata({
63
60
  managedCodexAgentFiles = getPreviousManagedCodexAgentFiles(previousMetadata),
64
61
  linkedTargets = {}
65
62
  }) {
66
- const metadata = {
63
+ return {
67
64
  schemaVersion: 2,
68
65
  packageVersion,
69
66
  managedFiles: sortObject(managedFiles),
@@ -71,10 +68,6 @@ function buildInstallMetadata({
71
68
  managedCodexAgentFiles: normalizeManagedCodexAgentFiles(managedCodexAgentFiles),
72
69
  linkedTargets: sortObject(linkedTargets)
73
70
  };
74
- if (typeof previousMetadata.installedAt === "string") {
75
- metadata.installedAt = previousMetadata.installedAt;
76
- }
77
- return metadata;
78
71
  }
79
72
 
80
73
  function finalizeProviderInstallMetadata({
@@ -100,17 +93,6 @@ function finalizeProviderInstallMetadata({
100
93
  });
101
94
  }
102
95
 
103
- function linkedTargetsFromResults(results) {
104
- return Object.fromEntries(results
105
- .filter((result) => result.sourcePath)
106
- .map((result) => [result.targetPath, {
107
- sourcePath: result.sourcePath,
108
- kind: result.kind,
109
- mode: result.mode,
110
- ...(result.targetHash ? { targetHash: result.targetHash } : {})
111
- }]));
112
- }
113
-
114
96
  function mergeLinkedTargets(previousLinkedTargets = {}, results = []) {
115
97
  const nextLinkedTargets = { ...previousLinkedTargets };
116
98
  for (const result of results) {
@@ -154,7 +136,6 @@ export {
154
136
  finalizeProviderInstallMetadata,
155
137
  getPreviousManagedCodexAgentFiles,
156
138
  getPreviousManagedCodexAuthKeys,
157
- linkedTargetsFromResults,
158
139
  mergeInstallReports,
159
140
  mergeLinkedTargets,
160
141
  metadataPathFor,
@@ -1,8 +1,5 @@
1
1
  import * as p from "@clack/prompts";
2
- import {
3
- readJsonFileSafe,
4
- writeJsonFileWithBackup
5
- } from "../config/store.mjs";
2
+ import { readJsonFileSafe, writeJson } from "../config/store.mjs";
6
3
  import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
7
4
 
8
5
  const claudeModelEnvKeys = [
@@ -111,18 +108,12 @@ function buildClaudeApiSettings(settings, {
111
108
  }
112
109
 
113
110
  async function persistClaudeConfiguration(paths, { settings, metaConfig }) {
114
- await writeJsonFileWithBackup(paths.claudeSettingsPath, settings, { sensitive: true });
115
- await writeJsonFileWithBackup(paths.claudeMetaConfigPath, metaConfig, { sensitive: true });
111
+ await writeJson(paths.claudeSettingsPath, settings, { sensitive: true });
112
+ await writeJson(paths.claudeMetaConfigPath, metaConfig, { sensitive: true });
116
113
  }
117
114
 
118
115
  async function configureClaudeApi(paths = defaultPaths, promptApi) {
119
- const {
120
- assertNotCancelled,
121
- passwordPromptOptions,
122
- required,
123
- requiredUnlessExisting,
124
- resolvePasswordValue
125
- } = promptApi;
116
+ const { assertNotCancelled, passwordOrExisting, required } = promptApi;
126
117
  const settings = await readJsonFileSafe(paths.claudeSettingsPath, {}, { sensitive: true });
127
118
  const existing = getExistingClaudeApiConfig(settings);
128
119
 
@@ -133,13 +124,10 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
133
124
  });
134
125
  assertNotCancelled(baseUrl);
135
126
 
136
- const key = await p.password(passwordPromptOptions(
137
- "Claude Code API Key",
138
- existing.key,
139
- requiredUnlessExisting(existing.key, "API Key 不能为空")
140
- ));
141
- assertNotCancelled(key);
142
- const finalKey = resolvePasswordValue(key, existing.key);
127
+ const finalKey = await passwordOrExisting({
128
+ message: "Claude Code API Key",
129
+ existingValue: existing.key
130
+ });
143
131
 
144
132
  const model = await p.text({
145
133
  message: "Claude Code 模型",
@@ -1,11 +1,10 @@
1
- import { createHash } from "node:crypto";
2
1
  import { lstat, mkdir, readFile, readdir, unlink } from "node:fs/promises";
3
2
  import { join } from "node:path";
4
3
  import * as p from "@clack/prompts";
5
4
  import {
6
5
  pathExists,
7
6
  readJsonFileSafe,
8
- writeJsonFileWithBackup,
7
+ writeJson,
9
8
  writeText
10
9
  } from "../config/store.mjs";
11
10
  import {
@@ -18,11 +17,13 @@ import {
18
17
  parseTomlSection,
19
18
  readTopLevelTomlString,
20
19
  removeTomlSection,
20
+ removeTomlSectionField,
21
21
  removeTopLevelTomlField,
22
22
  updateTomlSectionFields,
23
23
  updateTopLevelTomlField
24
24
  } from "../config/toml.mjs";
25
25
  import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
26
+ import { hashBytes, isManagedCodexAgentFileEntry } from "../utils.mjs";
26
27
  import { normalizeOpenAiBaseUrl } from "./pi.mjs";
27
28
 
28
29
  const CODEX_ENV_KEY = "OPENAI_API_KEY";
@@ -98,7 +99,7 @@ function mergeCodexTemplateDefaults(content, templateContent) {
98
99
  const currentDeveloperInstructions = readTopLevelTomlString(nextContent, "developer_instructions");
99
100
  const templateDeveloperInstructions = readTopLevelTomlString(templateContent, "developer_instructions");
100
101
  if (templateDeveloperInstructions && publishedCodexDeveloperInstructionHashes.has(
101
- sha256(currentDeveloperInstructions)
102
+ hashBytes(currentDeveloperInstructions)
102
103
  )) {
103
104
  nextContent = updateTopLevelTomlField(
104
105
  nextContent,
@@ -125,10 +126,6 @@ async function loadBundledCodexConfigTemplate(paths = defaultPaths) {
125
126
  return readFile(paths.codexTemplateConfigPath, "utf8");
126
127
  }
127
128
 
128
- function sha256(content) {
129
- return createHash("sha256").update(content).digest("hex");
130
- }
131
-
132
129
  async function readCodexAgentTarget(path) {
133
130
  try {
134
131
  const targetStat = await lstat(path);
@@ -175,14 +172,6 @@ async function ensureCodexAgentContainer(homeDir) {
175
172
  return targetDir;
176
173
  }
177
174
 
178
- function isCodexAgentOwnership(name, hash) {
179
- return name.endsWith(".toml")
180
- && !name.includes("/")
181
- && !name.includes("\\")
182
- && typeof hash === "string"
183
- && /^[a-f0-9]{64}$/u.test(hash);
184
- }
185
-
186
175
  function isPublishedCodexAgent(name, hash) {
187
176
  return publishedCodexAgentHashes[name]?.has(hash) ?? false;
188
177
  }
@@ -205,9 +194,9 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
205
194
  const source = join(paths.codexTemplateAgentsPath, name);
206
195
  const target = join(targetDir, name);
207
196
  const sourceContent = await readFile(source);
208
- const sourceHash = sha256(sourceContent);
197
+ const sourceHash = hashBytes(sourceContent);
209
198
  const current = await readCodexAgentTarget(target);
210
- const currentHash = current.content ? sha256(current.content) : "";
199
+ const currentHash = current.content ? hashBytes(current.content) : "";
211
200
 
212
201
  if (!current.exists) {
213
202
  await writeText(target, sourceContent, { backupLimit: 0 });
@@ -222,7 +211,7 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
222
211
  result.updated.push(target);
223
212
  } else {
224
213
  result.conflicts.push(target);
225
- if (isCodexAgentOwnership(name, previousManagedFiles?.[name])) {
214
+ if (isManagedCodexAgentFileEntry(name, previousManagedFiles?.[name])) {
226
215
  result.managedFiles[name] = previousManagedFiles[name];
227
216
  }
228
217
  continue;
@@ -237,11 +226,11 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
237
226
  ? Object.entries(previousManagedFiles).sort(([left], [right]) => left.localeCompare(right))
238
227
  : [];
239
228
  for (const [name, previousHash] of previousEntries) {
240
- if (bundledNames.has(name) || !isCodexAgentOwnership(name, previousHash)) continue;
229
+ if (bundledNames.has(name) || !isManagedCodexAgentFileEntry(name, previousHash)) continue;
241
230
  const target = join(targetDir, name);
242
231
  const current = await readCodexAgentTarget(target);
243
232
  if (!current.exists) continue;
244
- if (current.content && sha256(current.content) === previousHash) {
233
+ if (current.content && hashBytes(current.content) === previousHash) {
245
234
  await unlink(target);
246
235
  continue;
247
236
  }
@@ -291,17 +280,15 @@ async function getExistingCodexApiConfig(paths = defaultPaths) {
291
280
 
292
281
  async function persistCodexConfiguration(paths, { content, auth }) {
293
282
  await writeText(paths.codexConfigPath, content);
294
- await writeJsonFileWithBackup(paths.codexAuthPath, auth, { sensitive: true });
283
+ await writeJson(paths.codexAuthPath, auth, { sensitive: true });
295
284
  }
296
285
 
297
286
  async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}) {
298
287
  const {
299
288
  assertNotCancelled,
300
289
  confirmOrCancel,
301
- passwordPromptOptions,
302
- required,
303
- requiredUnlessExisting,
304
- resolvePasswordValue
290
+ passwordOrExisting,
291
+ required
305
292
  } = promptApi;
306
293
  const existing = await getExistingCodexApiConfig(paths);
307
294
  const providerId = existing.providerId || "abelworkflow";
@@ -314,13 +301,10 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
314
301
  assertNotCancelled(baseUrlInput);
315
302
  const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
316
303
 
317
- const apiKey = await p.password(passwordPromptOptions(
318
- "Codex 第三方 API Key",
319
- existing.apiKey,
320
- requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
321
- ));
322
- assertNotCancelled(apiKey);
323
- const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
304
+ const finalApiKey = await passwordOrExisting({
305
+ message: "Codex 第三方 API Key",
306
+ existingValue: existing.apiKey
307
+ });
324
308
 
325
309
  const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
326
310
  let managedCodexAgentFiles = { ...(ownership.managedCodexAgentFiles ?? {}) };
@@ -389,13 +373,16 @@ function buildCodexConfigContent(currentContent, {
389
373
  if (includeSubagentDefaults && readTopLevelTomlString(content, "approvals_reviewer") === "reviewer") {
390
374
  content = updateTopLevelTomlField(content, "approvals_reviewer", "guardian_subagent");
391
375
  }
376
+ const providerSectionName = getCodexProviderSectionName(providerId);
392
377
  content = updateTopLevelTomlField(content, "model_provider", providerId);
393
- content = updateTopLevelTomlField(content, "preferred_auth_method", "apikey");
394
- content = updateTomlSectionFields(content, getCodexProviderSectionName(providerId), {
378
+ content = removeTopLevelTomlField(content, "preferred_auth_method");
379
+ content = removeTopLevelTomlField(content, "temp_env_key");
380
+ content = removeTomlSectionField(content, providerSectionName, "temp_env_key");
381
+ content = removeTomlSectionField(content, providerSectionName, "env_key");
382
+ content = updateTomlSectionFields(content, providerSectionName, {
395
383
  name: providerName,
396
384
  base_url: baseUrl,
397
385
  wire_api: "responses",
398
- temp_env_key: CODEX_ENV_KEY,
399
386
  requires_openai_auth: true,
400
387
  supports_websockets: true
401
388
  });
@@ -408,8 +395,13 @@ function mergeCodexAuthData(auth, envKey, apiKey, managedAuthKeys = []) {
408
395
  for (const managedAuthKey of managedAuthKeys) {
409
396
  delete nextAuth[managedAuthKey];
410
397
  }
411
- if (apiKey) nextAuth[envKey] = apiKey;
412
- else delete nextAuth[envKey];
398
+ if (apiKey) {
399
+ nextAuth.auth_mode = "apikey";
400
+ nextAuth[envKey] = apiKey;
401
+ } else {
402
+ delete nextAuth[envKey];
403
+ if (nextAuth.auth_mode === "apikey") delete nextAuth.auth_mode;
404
+ }
413
405
  return nextAuth;
414
406
  }
415
407
 
@@ -5,7 +5,7 @@ import {
5
5
  readJsonFileSafe,
6
6
  readJsoncFileSafe,
7
7
  updateLockedJson,
8
- writeJsonFileWithBackup,
8
+ writeJson,
9
9
  writeText
10
10
  } from "../config/store.mjs";
11
11
  import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
@@ -412,7 +412,7 @@ async function persistPiConfiguration(paths, configuration, operations = {}) {
412
412
  `${JSON.stringify(value, null, 2)}\n`,
413
413
  { sensitive: true }
414
414
  ));
415
- const writeSettings = operations.writeSettings ?? ((path, value) => writeJsonFileWithBackup(path, value));
415
+ const writeSettings = operations.writeSettings ?? ((path, value) => writeJson(path, value));
416
416
  await updateAuth(paths.piAuthPath, configuration.providerId, configuration.apiKey);
417
417
  await writeModels(paths.piModelsPath, configuration.models);
418
418
  await writeSettings(paths.piSettingsPath, configuration.settings);
@@ -428,10 +428,8 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
428
428
  }
429
429
  const {
430
430
  assertNotCancelled,
431
- passwordPromptOptions,
431
+ passwordOrExisting,
432
432
  required,
433
- requiredUnlessExisting,
434
- resolvePasswordValue,
435
433
  selectOrCancel
436
434
  } = promptApi;
437
435
  const {
@@ -477,13 +475,10 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
477
475
  ...(initialApi ? { initialValue: initialApi } : {})
478
476
  });
479
477
 
480
- const apiKey = await p.password(passwordPromptOptions(
481
- `${providerLabel} API Key`,
482
- existing.apiKey,
483
- requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
484
- ));
485
- assertNotCancelled(apiKey);
486
- const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
478
+ const finalApiKey = await passwordOrExisting({
479
+ message: `${providerLabel} API Key`,
480
+ existingValue: existing.apiKey
481
+ });
487
482
 
488
483
  const modelIdsText = await p.text({
489
484
  message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
@@ -21,10 +21,8 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
21
21
  const {
22
22
  assertNotCancelled,
23
23
  confirmOrCancel,
24
- passwordPromptOptions,
25
- required,
26
- requiredUnlessExisting,
27
- resolvePasswordValue
24
+ passwordOrExisting,
25
+ required
28
26
  } = promptApi;
29
27
  await ensureSkillPresent(paths);
30
28
  const envPath = join(paths.agentsDir, "skills", "grok-search", ".env");
@@ -36,13 +34,11 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
36
34
  });
37
35
  assertNotCancelled(baseUrl);
38
36
 
39
- const apiKey = await p.password(passwordPromptOptions(
40
- "Grok API Key",
41
- existing.GROK_API_KEY,
42
- requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
43
- ));
44
- assertNotCancelled(apiKey);
45
- const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
37
+ const finalApiKey = await passwordOrExisting({
38
+ message: "Grok API Key",
39
+ existingValue: existing.GROK_API_KEY,
40
+ requiredMessage: "Grok API Key 不能为空"
41
+ });
46
42
 
47
43
  const model = await p.text({
48
44
  message: "Grok 默认模型",
@@ -65,15 +61,13 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
65
61
  : null;
66
62
  if (useTavily) assertNotCancelled(tavilyUrl);
67
63
 
68
- const tavilyKey = useTavily
69
- ? await p.password(passwordPromptOptions(
70
- "Tavily API Key",
71
- existing.TAVILY_API_KEY,
72
- requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
73
- ))
74
- : "";
75
- if (useTavily) assertNotCancelled(tavilyKey);
76
- const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
64
+ const finalTavilyKey = useTavily
65
+ ? await passwordOrExisting({
66
+ message: "Tavily API Key",
67
+ existingValue: existing.TAVILY_API_KEY,
68
+ requiredMessage: "Tavily API Key 不能为空"
69
+ })
70
+ : null;
77
71
 
78
72
  await updateSkillEnvFile(envPath, {
79
73
  GROK_API_URL: baseUrl,
@@ -88,16 +82,15 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
88
82
  }
89
83
 
90
84
  async function configureContext7Env(paths, ensureSkillPresent = async () => {}, promptApi) {
91
- const { assertNotCancelled, passwordPromptOptions, resolvePasswordValue } = promptApi;
85
+ const { passwordOrExisting } = promptApi;
92
86
  await ensureSkillPresent(paths);
93
87
  const envPath = join(paths.agentsDir, "skills", "context7-auto-research", ".env");
94
88
  const existing = await readSkillEnvFile(envPath);
95
- const apiKey = await p.password(passwordPromptOptions(
96
- "Context7 API Key(可选)",
97
- existing.CONTEXT7_API_KEY
98
- ));
99
- assertNotCancelled(apiKey);
100
- const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
89
+ const finalApiKey = await passwordOrExisting({
90
+ message: "Context7 API Key(可选)",
91
+ existingValue: existing.CONTEXT7_API_KEY,
92
+ requiredMessage: null
93
+ });
101
94
 
102
95
  await updateSkillEnvFile(envPath, {
103
96
  CONTEXT7_API_KEY: finalApiKey
@@ -1,10 +1,9 @@
1
1
  personality = "pragmatic"
2
2
  model_provider = "abelworkflow"
3
3
  disable_response_storage = true
4
- preferred_auth_method = "apikey"
5
4
  approvals_reviewer = "guardian_subagent"
6
5
  approval_policy = "on-request"
7
- sandbox_mode = "workspace-write"
6
+ sandbox_mode = "danger-full-access"
8
7
  model = "gpt-5.6-sol"
9
8
  model_reasoning_effort = "high"
10
9
  network_access = true
package/lib/utils.mjs ADDED
@@ -0,0 +1,15 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ function hashBytes(content) {
4
+ return createHash("sha256").update(content).digest("hex");
5
+ }
6
+
7
+ function isManagedCodexAgentFileEntry(name, hash) {
8
+ return name.endsWith(".toml")
9
+ && !name.includes("/")
10
+ && !name.includes("\\")
11
+ && typeof hash === "string"
12
+ && /^[a-f0-9]{64}$/u.test(hash);
13
+ }
14
+
15
+ export { hashBytes, isManagedCodexAgentFileEntry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "scripts": {