@openagentpack/sdk 0.6.0 → 0.7.1
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 +17 -0
- package/dist/chunk-56BB46YT.js +1191 -0
- package/dist/chunk-HIK5DY4I.js +2429 -0
- package/dist/chunk-ZBJDMUJT.js +6989 -0
- package/dist/directory-nY2Slsfi.d.ts +98 -0
- package/dist/{session-event-B63Rbjim.d.ts → dto-BT1Q1Ii6.d.ts} +1 -50
- package/dist/errors-BcVE1wZB.d.ts +5 -0
- package/dist/index.d.ts +7 -1128
- package/dist/index.js +249 -9534
- package/dist/project-versions.d.ts +105 -0
- package/dist/project-versions.js +35 -0
- package/dist/project-workspace.d.ts +150 -0
- package/dist/project-workspace.js +1774 -0
- package/dist/resource-runtime-DZDY45_Q.d.ts +1126 -0
- package/dist/session-event-DB_YlDiK.d.ts +52 -0
- package/dist/session-events.d.ts +2 -1
- package/package.json +13 -1
|
@@ -0,0 +1,1774 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LocalFileStateBackend,
|
|
3
|
+
executePlannedProject,
|
|
4
|
+
planProjectContext,
|
|
5
|
+
planProjectWithStateBackend,
|
|
6
|
+
writeProjectRuntime
|
|
7
|
+
} from "./chunk-HIK5DY4I.js";
|
|
8
|
+
import {
|
|
9
|
+
createDirectoryProjectVersionService
|
|
10
|
+
} from "./chunk-56BB46YT.js";
|
|
11
|
+
import {
|
|
12
|
+
UserError,
|
|
13
|
+
inspectProjectSource,
|
|
14
|
+
resolveProjectConfig,
|
|
15
|
+
resolveProjectConfigFromObject,
|
|
16
|
+
validateProjectConfig
|
|
17
|
+
} from "./chunk-ZBJDMUJT.js";
|
|
18
|
+
import "./chunk-6PDVAFLK.js";
|
|
19
|
+
import "./chunk-CHGDM6TX.js";
|
|
20
|
+
|
|
21
|
+
// src/internal/project-workspace/index.ts
|
|
22
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
23
|
+
import {
|
|
24
|
+
chmod as chmod2,
|
|
25
|
+
cp,
|
|
26
|
+
lstat as lstat2,
|
|
27
|
+
mkdir,
|
|
28
|
+
readdir,
|
|
29
|
+
readFile as readFile2,
|
|
30
|
+
realpath,
|
|
31
|
+
rename as rename2,
|
|
32
|
+
rm as rm2,
|
|
33
|
+
rmdir,
|
|
34
|
+
stat,
|
|
35
|
+
writeFile as writeFile2
|
|
36
|
+
} from "fs/promises";
|
|
37
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve as resolve2, sep } from "path";
|
|
38
|
+
import { parse, stringify } from "yaml";
|
|
39
|
+
|
|
40
|
+
// src/internal/project-workspace/scaffold.ts
|
|
41
|
+
var RESOURCE_EXAMPLES_DIRECTORY = "_examples";
|
|
42
|
+
function directoryProjectScaffold() {
|
|
43
|
+
const agentRoot = "agents/assistant";
|
|
44
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}
|
|
45
|
+
`;
|
|
46
|
+
return {
|
|
47
|
+
"project.json": json({ version: "1" }),
|
|
48
|
+
[`${agentRoot}/agent.json`]: json({
|
|
49
|
+
name: "Assistant",
|
|
50
|
+
model: "qwen3.7-max"
|
|
51
|
+
}),
|
|
52
|
+
[`${agentRoot}/instructions.md`]: "You are a helpful assistant.\n",
|
|
53
|
+
[`${agentRoot}/skills/${RESOURCE_EXAMPLES_DIRECTORY}/example-skill/skill.json`]: json({
|
|
54
|
+
id: "example-skill",
|
|
55
|
+
name: "Example Skill",
|
|
56
|
+
description: "Summarize user-provided text. / \u603B\u7ED3\u7528\u6237\u63D0\u4F9B\u7684\u6587\u672C\u3002"
|
|
57
|
+
}),
|
|
58
|
+
[`${agentRoot}/skills/${RESOURCE_EXAMPLES_DIRECTORY}/example-skill/SKILL.md`]: `---
|
|
59
|
+
name: example-skill
|
|
60
|
+
description: Summarize user-provided text into concise key points. \u603B\u7ED3\u7528\u6237\u63D0\u4F9B\u7684\u6587\u672C\u3002
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# Summarize text / \u6587\u672C\u603B\u7ED3
|
|
64
|
+
|
|
65
|
+
Use this Skill when the user asks to summarize text.
|
|
66
|
+
\u7528\u6237\u8981\u6C42\u603B\u7ED3\u6587\u672C\u65F6\uFF0C\u4F7F\u7528\u6B64 Skill\u3002
|
|
67
|
+
|
|
68
|
+
1. Read the provided text. / \u9605\u8BFB\u7528\u6237\u63D0\u4F9B\u7684\u6587\u672C\u3002
|
|
69
|
+
2. Extract the main ideas without adding unsupported facts. / \u63D0\u70BC\u8981\u70B9\uFF0C\u4E0D\u6DFB\u52A0\u672A\u7ECF\u652F\u6301\u7684\u4E8B\u5B9E\u3002
|
|
70
|
+
3. Respond in the user's language with a brief summary. / \u4F7F\u7528\u7528\u6237\u7684\u8BED\u8A00\u7ED9\u51FA\u7B80\u77ED\u603B\u7ED3\u3002
|
|
71
|
+
`,
|
|
72
|
+
[`${agentRoot}/skills/${RESOURCE_EXAMPLES_DIRECTORY}/example-skill/README.md`]: `# Skill example / Skill \u914D\u7F6E\u793A\u4F8B
|
|
73
|
+
|
|
74
|
+
- \`skill.json\` defines the resource ID and display fields. Keep its \`id\` unique across the project.
|
|
75
|
+
\`skill.json\` \u58F0\u660E\u8D44\u6E90 ID \u548C\u5C55\u793A\u5B57\u6BB5\uFF0CID \u5728\u9879\u76EE\u7684 Skill \u4E2D\u5FC5\u987B\u552F\u4E00\u3002
|
|
76
|
+
- \`SKILL.md\` contains the Skill instructions. Put scripts/assets in this same directory if needed;
|
|
77
|
+
the directory is the upload source. Do not put secrets in these files.
|
|
78
|
+
\`SKILL.md\` \u662F Skill \u6307\u4EE4\uFF0C\u9700\u8981\u7684\u811A\u672C\u548C\u7D20\u6750\u4E5F\u653E\u5728\u6B64\u76EE\u5F55\uFF1B\u6574\u4E2A\u76EE\u5F55\u4F5C\u4E3A\u4E0A\u4F20\u6E90\uFF0C\u8BF7\u52FF\u653E\u5165\u5BC6\u94A5\u3002
|
|
79
|
+
- This example is ignored by Build/Publish. Copy it from \`skills/_examples/example-skill/\`
|
|
80
|
+
to \`skills/example-skill/\`, then Build adds \`"example-skill"\` to \`agent.json.skills\` automatically.
|
|
81
|
+
\u6B64\u793A\u4F8B\u4E0D\u53C2\u4E0E\u6784\u5EFA/\u53D1\u5E03\u3002\u590D\u5236\u5230 \`skills/example-skill/\` \u540E\uFF0CBuild \u4F1A\u81EA\u52A8\u8865\u9F50 Agent \u5F15\u7528\u3002
|
|
82
|
+
- To add another Skill, copy this directory, change the directory name and \`skill.json.id\`,
|
|
83
|
+
then run Build to add its ID to \`agent.json.skills\`. A new directory with only \`SKILL.md\` is also discovered by Build.
|
|
84
|
+
\u65B0\u589E Skill \u65F6\u590D\u5236\u6B64\u76EE\u5F55\u3001\u4FEE\u6539\u76EE\u5F55\u540D\u548C ID\uFF0C\u518D\u8FD0\u884C Build \u81EA\u52A8\u5173\u8054\uFF1B\u53EA\u653E \`SKILL.md\` \u7684\u65B0\u76EE\u5F55\u4E5F\u80FD\u7531 Build \u81EA\u52A8\u5173\u8054\u3002
|
|
85
|
+
- Delete this ignored example directory if not needed. For an enabled Skill, also remove its Agent reference.
|
|
86
|
+
\u4E0D\u9700\u8981\u65F6\u53EF\u76F4\u63A5\u5220\u9664\u672C\u793A\u4F8B\u76EE\u5F55\uFF1B\u82E5\u5DF2\u7ECF\u542F\u7528\uFF0C\u8FD8\u9700\u5220\u9664 Agent \u5F15\u7528\u3002
|
|
87
|
+
`,
|
|
88
|
+
[`${agentRoot}/files/${RESOURCE_EXAMPLES_DIRECTORY}/example-file/file.json`]: json({
|
|
89
|
+
id: "example-file",
|
|
90
|
+
name: "example.md",
|
|
91
|
+
source: "./example.md"
|
|
92
|
+
}),
|
|
93
|
+
[`${agentRoot}/files/${RESOURCE_EXAMPLES_DIRECTORY}/example-file/example.md`]: `# Example reference / \u793A\u4F8B\u53C2\u8003\u6587\u4EF6
|
|
94
|
+
|
|
95
|
+
Replace this file with the reference material your Agent needs.
|
|
96
|
+
\u8BF7\u5C06\u6B64\u6587\u4EF6\u66FF\u6362\u4E3A Agent \u9700\u8981\u7684\u53C2\u8003\u8D44\u6599\u3002
|
|
97
|
+
|
|
98
|
+
This is an ignored example. After you explicitly enable it, configure a new-Session mount
|
|
99
|
+
at \`/mnt/example.md\` in agent.json.
|
|
100
|
+
\u8FD9\u662F\u672A\u542F\u7528\u7684\u793A\u4F8B\u3002\u663E\u5F0F\u542F\u7528\u540E\uFF0C\u53EF\u5728 agent.json \u4E2D\u914D\u7F6E\u65B0 Session \u7684\u6302\u8F7D\u8DEF\u5F84 \`/mnt/example.md\`\u3002
|
|
101
|
+
`,
|
|
102
|
+
[`${agentRoot}/files/${RESOURCE_EXAMPLES_DIRECTORY}/example-file/README.md`]: `# File example / File \u914D\u7F6E\u793A\u4F8B
|
|
103
|
+
|
|
104
|
+
- \`file.json\` is metadata; \`example.md\` is the content uploaded by Publish.
|
|
105
|
+
\`file.json\` \u662F\u5143\u6570\u636E\uFF1BPublish \u4E0A\u4F20\u7684\u662F \`example.md\` \u5185\u5BB9\uFF0C\u4E0D\u662F\u672C README\u3002
|
|
106
|
+
- \`source\` is relative to this resource directory. Keep \`id\` equal to the directory name.
|
|
107
|
+
\`source\` \u76F8\u5BF9\u6B64\u8D44\u6E90\u76EE\u5F55\u89E3\u6790\uFF0C\`id\` \u5E94\u4E0E\u76EE\u5F55\u540D\u4E00\u81F4\u3002
|
|
108
|
+
- Build/Publish ignore this example. Copy \`files/_examples/example-file/\` to \`files/example-file/\`
|
|
109
|
+
to enable its declaration; Build adds \`{"file": "example-file", "mount_path": "/mnt/example.md"}\` to \`agent.json.files\`.
|
|
110
|
+
\u6B64\u793A\u4F8B\u4E0D\u53C2\u4E0E\u6784\u5EFA/\u53D1\u5E03\u3002\u590D\u5236\u5230 \`files/example-file/\` \u540E\uFF0CBuild \u81EA\u52A8\u6DFB\u52A0\u4E0A\u8FF0\u6302\u8F7D\u5F15\u7528\uFF1B\u5DF2\u6709\u81EA\u5B9A\u4E49\u6302\u8F7D\u8DEF\u5F84\u4F1A\u4FDD\u7559\uFF0C\u6302\u8F7D\u8DEF\u5F84\u5FC5\u987B\u5728 \`/mnt/\` \u4E0B\u3002
|
|
111
|
+
- Uploading a File does not permanently attach it to the remote Agent. These declarations
|
|
112
|
+
supply default mounts for new Sessions; existing Sessions are unchanged.
|
|
113
|
+
\u4E0A\u4F20 File \u4E0D\u7B49\u4E8E\u6C38\u4E45\u7ED1\u5B9A\u5230\u8FDC\u7AEF Agent\uFF1B\u8FD9\u91CC\u58F0\u660E\u7684\u662F\u65B0 Session \u7684\u9ED8\u8BA4\u6302\u8F7D\uFF0C\u4E0D\u4FEE\u6539\u5DF2\u6709 Session\u3002
|
|
114
|
+
- Build can also infer metadata and an Agent mount for a file placed directly in the parent
|
|
115
|
+
\`files/\` directory, or a resource directory containing exactly one content file.
|
|
116
|
+
\u5728\u4E0A\u7EA7 \`files/\` \u76F4\u63A5\u653E\u6587\u4EF6\uFF0C\u6216\u5728\u65B0\u8D44\u6E90\u76EE\u5F55\u4E2D\u53EA\u653E\u4E00\u4E2A\u5185\u5BB9\u6587\u4EF6\uFF0CBuild \u4E5F\u4F1A\u81EA\u52A8\u751F\u6210\u5143\u6570\u636E\u548C\u6302\u8F7D\u5F15\u7528\u3002
|
|
117
|
+
- Delete this ignored example directory if not needed. For an enabled File, also remove its Agent reference.
|
|
118
|
+
\u4E0D\u9700\u8981\u65F6\u53EF\u76F4\u63A5\u5220\u9664\u672C\u793A\u4F8B\u76EE\u5F55\uFF1B\u82E5\u5DF2\u7ECF\u542F\u7528\uFF0C\u8FD8\u9700\u5220\u9664 Agent \u4E2D\u7684 File \u5F15\u7528\u3002
|
|
119
|
+
`,
|
|
120
|
+
[`${agentRoot}/vaults/${RESOURCE_EXAMPLES_DIRECTORY}/example-vault/vault.json`]: json({
|
|
121
|
+
id: "example-vault",
|
|
122
|
+
display_name: "Example Vault",
|
|
123
|
+
credentials: [
|
|
124
|
+
{
|
|
125
|
+
name: "service-token",
|
|
126
|
+
type: "environment_variable",
|
|
127
|
+
secret_name: "SERVICE_TOKEN",
|
|
128
|
+
secret_value: "${SERVICE_TOKEN}"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}),
|
|
132
|
+
[`${agentRoot}/vaults/${RESOURCE_EXAMPLES_DIRECTORY}/example-vault/README.md`]: `# Vault example / Vault \u914D\u7F6E\u793A\u4F8B
|
|
133
|
+
|
|
134
|
+
Build/Publish ignore this directory; no example secret is required to open Workbench.
|
|
135
|
+
Copy \`vaults/_examples/example-vault/\` to \`vaults/example-vault/\` to enable the declaration,
|
|
136
|
+
then supply the secret and run Build. If \`agent.json.vault\` is unset, Build selects the sole local Vault automatically.
|
|
137
|
+
With multiple local Vaults, set \`"vault": "example-vault"\` explicitly; Build never overwrites an existing selection.
|
|
138
|
+
\u6B64\u793A\u4F8B\u4E0D\u53C2\u4E0E\u6784\u5EFA/\u53D1\u5E03\uFF0C\u4E0D\u9700\u8981\u914D\u7F6E\u5BC6\u94A5\u5373\u53EF\u6253\u5F00 Workbench\u3002
|
|
139
|
+
\u542F\u7528\u65F6\uFF0C\u590D\u5236\u5230 \`vaults/example-vault/\`\uFF0C\u914D\u7F6E\u5BC6\u94A5\u540E\u8FD0\u884C Build\u3002\u672A\u6307\u5B9A Vault \u65F6\u81EA\u52A8\u5173\u8054\u552F\u4E00\u5019\u9009\uFF1B\u591A\u4E2A\u5019\u9009\u9700\u663E\u5F0F\u9009\u62E9\uFF0C\u5DF2\u6709\u5F15\u7528\u4E0D\u8986\u76D6\u3002
|
|
140
|
+
|
|
141
|
+
The example \`vault.json\` contains:
|
|
142
|
+
\u793A\u4F8B \`vault.json\` \u7684\u5B8C\u6574\u914D\u7F6E\u5982\u4E0B\uFF1A
|
|
143
|
+
|
|
144
|
+
\`\`\`json
|
|
145
|
+
${JSON.stringify(
|
|
146
|
+
{
|
|
147
|
+
id: "example-vault",
|
|
148
|
+
display_name: "Example Vault",
|
|
149
|
+
credentials: [
|
|
150
|
+
{
|
|
151
|
+
name: "service-token",
|
|
152
|
+
type: "environment_variable",
|
|
153
|
+
secret_name: "SERVICE_TOKEN",
|
|
154
|
+
secret_value: "${SERVICE_TOKEN}"
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
null,
|
|
159
|
+
2
|
|
160
|
+
)}
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
- \`name\` identifies the credential; \`secret_name\` is the environment variable exposed to the runtime.
|
|
164
|
+
\`secret_value\` references a local environment variable. The two variable names may differ.
|
|
165
|
+
\`name\` \u662F\u51ED\u636E\u540D\u79F0\uFF1B\`secret_name\` \u662F\u8FD0\u884C\u65F6\u7684\u53D8\u91CF\u540D\uFF1B\`secret_value\` \u5F15\u7528\u672C\u5730\u73AF\u5883\u53D8\u91CF\uFF0C\u4E24\u8005\u53EF\u4EE5\u4E0D\u540C\u3002
|
|
166
|
+
- Provide \`SERVICE_TOKEN\` in the process environment or project-root \`.env\` before runtime/Publish.
|
|
167
|
+
\u4F7F\u7528\u524D\u5728\u8FDB\u7A0B\u73AF\u5883\u6216\u9879\u76EE\u6839\u76EE\u5F55 \`.env\` \u4E2D\u914D\u7F6E \`SERVICE_TOKEN\`\uFF0C\u4E0D\u8981\u5C06\u771F\u5B9E\u503C\u5199\u5165\u672C README\u3002
|
|
168
|
+
- If you put a literal secret in \`vault.json\`, Build moves it to project-root \`.env\`
|
|
169
|
+
and replaces the JSON value with an environment reference. Preview/dry-run do not write it.
|
|
170
|
+
\u82E5\u5728 \`vault.json\` \u5199\u5165\u660E\u6587\uFF0CBuild \u4F1A\u5C06\u5176\u79FB\u5165\u9879\u76EE\u6839\u76EE\u5F55 \`.env\` \u5E76\u5199\u56DE\u53D8\u91CF\u5F15\u7528\uFF1B\u9884\u89C8\u4E0D\u4F1A\u5199\u6587\u4EF6\u3002
|
|
171
|
+
- \`.env\` is plaintext with owner-only permissions when created by Build. It is excluded from local
|
|
172
|
+
version snapshots, not automatically Git-ignored. Back it up securely and add it to your Git ignore rules.
|
|
173
|
+
\`.env\` \u4ECD\u662F\u672C\u5730\u660E\u6587\u6587\u4EF6\uFF1BBuild \u521B\u5EFA\u65F6\u4EC5\u6587\u4EF6\u6240\u6709\u8005\u53EF\u8BFB\u5199\u3002\u5B83\u4E0D\u8FDB\u5165\u672C\u5730\u7248\u672C\u5FEB\u7167\uFF0C\u8BF7\u5B89\u5168\u5907\u4EFD\u5E76\u81EA\u884C\u52A0\u5165 Git \u5FFD\u7565\u89C4\u5219\u3002
|
|
174
|
+
- Never put real credentials into ignored examples: example files are still local versioned source.
|
|
175
|
+
Do not put secrets into this README. Only enabled Vault declarations participate in Build secret migration.
|
|
176
|
+
\u4E0D\u8981\u5728\u793A\u4F8B\u6216 README \u4E2D\u653E\u5165\u771F\u5B9E\u5BC6\u94A5\uFF1A\u793A\u4F8B\u4ECD\u5C5E\u4E8E\u672C\u5730\u7248\u672C\u6E90\u6587\u4EF6\u3002\u53EA\u6709\u5DF2\u542F\u7528\u7684 Vault \u58F0\u660E\u4F1A\u53C2\u4E0E Build \u5BC6\u94A5\u8FC1\u79FB\u3002
|
|
177
|
+
- Delete this ignored example directory if not needed. For an enabled Vault, also remove \`agent.json.vault\`.
|
|
178
|
+
\u4E0D\u9700\u8981\u65F6\u53EF\u76F4\u63A5\u5220\u9664\u672C\u793A\u4F8B\u76EE\u5F55\uFF1B\u82E5\u5DF2\u7ECF\u542F\u7528\uFF0C\u8FD8\u9700\u5220\u9664 Agent \u7684 \`vault\` \u5B57\u6BB5\u3002
|
|
179
|
+
`,
|
|
180
|
+
[`${agentRoot}/environments/${RESOURCE_EXAMPLES_DIRECTORY}/example-env/environment.json`]: json({
|
|
181
|
+
id: "example-env",
|
|
182
|
+
name: "Example Environment",
|
|
183
|
+
config: { type: "cloud" }
|
|
184
|
+
}),
|
|
185
|
+
[`${agentRoot}/environments/${RESOURCE_EXAMPLES_DIRECTORY}/example-env/README.md`]: `# Environment example / Environment \u914D\u7F6E\u793A\u4F8B
|
|
186
|
+
|
|
187
|
+
- This is an ignored managed cloud environment example. Copy \`environments/_examples/example-env/\`
|
|
188
|
+
to \`environments/example-env/\`, then run Build. An unset \`agent.json.environment\` is linked to the sole local Environment.
|
|
189
|
+
With multiple local Environments, set \`"environment": "example-env"\` explicitly; existing selections are preserved.
|
|
190
|
+
\u8FD9\u662F\u4E0D\u53C2\u4E0E\u6784\u5EFA/\u53D1\u5E03\u7684\u6258\u7BA1\u4E91\u73AF\u5883\u793A\u4F8B\u3002\u590D\u5236\u5230 \`environments/example-env/\` \u540E\u8FD0\u884C Build\uFF1B\u672A\u6307\u5B9A\u73AF\u5883\u65F6\u81EA\u52A8\u5173\u8054\u552F\u4E00\u5019\u9009\uFF0C\u591A\u4E2A\u5019\u9009\u9700\u663E\u5F0F\u9009\u62E9\uFF0C\u5DF2\u6709\u5F15\u7528\u4E0D\u8986\u76D6\u3002
|
|
191
|
+
Keep \`id\` equal to the directory name. / ID \u5E94\u4E0E\u76EE\u5F55\u540D\u4E00\u81F4\u3002
|
|
192
|
+
- Optional \`config\` fields include \`networking\`, \`packages\`, and \`setup_script\`.
|
|
193
|
+
\`config\` \u53EF\u6309\u9700\u589E\u52A0\u7F51\u7EDC\u7B56\u7565\u3001\u4F9D\u8D56\u5305\u548C\u521D\u59CB\u5316\u811A\u672C\uFF0C\u4F8B\u5982\uFF1A
|
|
194
|
+
|
|
195
|
+
\`\`\`json
|
|
196
|
+
${JSON.stringify({ type: "cloud", packages: { pip: ["requests"] }, setup_script: "echo 'Environment ready'" }, null, 2)}
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
- The snippet replaces only \`config\`. Build validates declarations locally;
|
|
200
|
+
packages/scripts take effect on the remote platform, not on your computer during Init/Build.
|
|
201
|
+
\u4E0A\u9762\u7684\u7247\u6BB5\u53EA\u66FF\u6362 \`config\`\u3002Init/Build \u4E0D\u4F1A\u5728\u672C\u673A\u5B89\u88C5\u4F9D\u8D56\u6216\u6267\u884C\u811A\u672C\uFF1B\u8FDC\u7AEF\u662F\u5426\u652F\u6301\u4EE5 Provider \u4E3A\u51C6\u3002
|
|
202
|
+
- Init leaves \`agent.json\` unchanged and only creates ignored resource examples.
|
|
203
|
+
Moving a declaration outside \`_examples/\` enables it for Build/Publish; Build also fills in the owning Agent's missing reference.
|
|
204
|
+
Init \u4E0D\u6DFB\u52A0 Agent \u8D44\u6E90\u5F15\u7528\u3002\u5C06\u58F0\u660E\u79FB\u5230 \`_examples/\` \u5916\u540E\uFF0CBuild \u4F1A\u8865\u9F50\u6240\u5C5E Agent \u7684\u5F15\u7528\u5E76\u7EB3\u5165\u6784\u5EFA/\u53D1\u5E03\u3002
|
|
205
|
+
- Delete this ignored example directory if not needed. For an enabled Environment, also remove its Agent reference.
|
|
206
|
+
\u4E0D\u9700\u8981\u65F6\u53EF\u76F4\u63A5\u5220\u9664\u672C\u793A\u4F8B\u76EE\u5F55\uFF1B\u82E5\u5DF2\u7ECF\u542F\u7528\uFF0C\u8FD8\u9700\u5220\u9664 Agent \u7684 Environment \u5F15\u7528\u3002
|
|
207
|
+
`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/internal/project-workspace/vault-secrets.ts
|
|
212
|
+
import { createHash, randomUUID } from "crypto";
|
|
213
|
+
import { chmod, lstat, readFile, rename, rm, writeFile } from "fs/promises";
|
|
214
|
+
import { resolve } from "path";
|
|
215
|
+
import * as nodeUtil from "util";
|
|
216
|
+
var ENV_REFERENCE = /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/;
|
|
217
|
+
var VAULT_PATH = /^(?:agents\/[^/]+\/vaults|resources\/vaults)\/[^/]+\/vault\.json$/;
|
|
218
|
+
async function planVaultSecretMigration(root, files) {
|
|
219
|
+
const plan = { overrides: /* @__PURE__ */ new Map(), literals: [], count: 0, envBefore: null, envAfter: null };
|
|
220
|
+
const candidates = [];
|
|
221
|
+
for (const file of files.filter(
|
|
222
|
+
(candidate) => VAULT_PATH.test(candidate.path) && !candidate.path.includes(`/vaults/${RESOURCE_EXAMPLES_DIRECTORY}/`)
|
|
223
|
+
)) {
|
|
224
|
+
let metadata;
|
|
225
|
+
try {
|
|
226
|
+
metadata = JSON.parse(Buffer.from(file.content).toString("utf8"));
|
|
227
|
+
} catch {
|
|
228
|
+
throw new UserError(`${file.path}: invalid JSON; source omitted. / JSON \u65E0\u6548\uFF0C\u5DF2\u9690\u85CF\u6E90\u5185\u5BB9\u3002`);
|
|
229
|
+
}
|
|
230
|
+
if (!isRecord(metadata) || !Array.isArray(metadata.credentials)) continue;
|
|
231
|
+
for (const credential of metadata.credentials) {
|
|
232
|
+
if (!isRecord(credential)) continue;
|
|
233
|
+
for (const field of ["secret_value", "access_token"]) {
|
|
234
|
+
const value = credential[field];
|
|
235
|
+
if (typeof value !== "string" && typeof value !== "number" || String(value) === "" || ENV_REFERENCE.test(String(value)))
|
|
236
|
+
continue;
|
|
237
|
+
candidates.push({ path: file.path, metadata, credential, field, value: String(value) });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (candidates.length === 0) return plan;
|
|
242
|
+
plan.envBefore = await readProjectDotEnv(root);
|
|
243
|
+
const existing = parseProjectDotEnv(plan.envBefore ?? "");
|
|
244
|
+
let content = plan.envBefore ?? "";
|
|
245
|
+
for (const candidate of candidates) {
|
|
246
|
+
const identity = `${String(candidate.metadata.id)}\0${String(candidate.credential.name)}\0${candidate.field}`;
|
|
247
|
+
const suffix = createHash("sha256").update(identity).digest("hex").slice(0, 12).toUpperCase();
|
|
248
|
+
const label = `${String(candidate.metadata.id)}_${String(candidate.credential.name)}_${candidate.field}`.replace(/[^A-Za-z0-9_]/g, "_").toUpperCase().slice(0, 80);
|
|
249
|
+
const baseName = `AGENTS_VAULT_${label}_${suffix}`;
|
|
250
|
+
let variable = baseName;
|
|
251
|
+
let sequence = 2;
|
|
252
|
+
while (existing[variable] !== void 0 && existing[variable] !== candidate.value || process.env[variable] !== void 0 && process.env[variable] !== candidate.value) {
|
|
253
|
+
variable = `${baseName}_${sequence++}`;
|
|
254
|
+
}
|
|
255
|
+
if (existing[variable] === void 0) {
|
|
256
|
+
const assignment = encodeAssignment(variable, candidate.value);
|
|
257
|
+
if (content && !content.endsWith("\n")) content += "\n";
|
|
258
|
+
content += assignment;
|
|
259
|
+
existing[variable] = candidate.value;
|
|
260
|
+
}
|
|
261
|
+
candidate.credential[candidate.field] = `\${${variable}}`;
|
|
262
|
+
plan.overrides.set(candidate.path, candidate.metadata);
|
|
263
|
+
plan.literals.push(candidate.value);
|
|
264
|
+
plan.count++;
|
|
265
|
+
}
|
|
266
|
+
const verified = parseProjectDotEnv(content);
|
|
267
|
+
for (const [name, value] of Object.entries(existing)) {
|
|
268
|
+
if (verified[name] !== value)
|
|
269
|
+
throw new UserError("Cannot safely append to project .env. Fix its syntax first. / \u8BF7\u5148\u4FEE\u590D\u9879\u76EE .env \u7684\u8BED\u6CD5\u3002");
|
|
270
|
+
}
|
|
271
|
+
plan.envAfter = content;
|
|
272
|
+
return plan;
|
|
273
|
+
}
|
|
274
|
+
async function applyVaultSecretMigration(root, files, plan) {
|
|
275
|
+
if (plan.count === 0) return;
|
|
276
|
+
const assertEnvironmentUnchanged = async () => {
|
|
277
|
+
if (await readProjectDotEnv(root) !== plan.envBefore)
|
|
278
|
+
throw new UserError("Project .env changed. Preview Build again. / .env \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u6784\u5EFA\u3002");
|
|
279
|
+
};
|
|
280
|
+
await assertEnvironmentUnchanged();
|
|
281
|
+
const sourceByPath = new Map(files.map((file) => [file.path, file]));
|
|
282
|
+
for (const path of plan.overrides.keys()) await assertUnchanged(root, sourceByPath.get(path));
|
|
283
|
+
await writePrivateAtomic(resolve(root, ".env"), plan.envAfter, 384, assertEnvironmentUnchanged);
|
|
284
|
+
for (const [path, metadata] of plan.overrides) {
|
|
285
|
+
const source = sourceByPath.get(path);
|
|
286
|
+
await assertUnchanged(root, source);
|
|
287
|
+
await writePrivateAtomic(
|
|
288
|
+
resolve(root, path),
|
|
289
|
+
`${JSON.stringify(metadata, null, 2)}
|
|
290
|
+
`,
|
|
291
|
+
source.mode,
|
|
292
|
+
() => assertUnchanged(root, source)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function readProjectEnvironment(root) {
|
|
297
|
+
return parseProjectDotEnv(await readProjectDotEnv(root) ?? "");
|
|
298
|
+
}
|
|
299
|
+
async function readProjectDotEnv(root) {
|
|
300
|
+
const path = resolve(root, ".env");
|
|
301
|
+
try {
|
|
302
|
+
const details = await lstat(path);
|
|
303
|
+
if (!details.isFile() || details.isSymbolicLink()) throw new Error("unsafe file");
|
|
304
|
+
return await readFile(path, "utf8");
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
307
|
+
throw new UserError(
|
|
308
|
+
"Project .env must be a readable regular file, not a symlink. / \u9879\u76EE .env \u5FC5\u987B\u662F\u53EF\u8BFB\u7684\u666E\u901A\u6587\u4EF6\uFF0C\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5\u3002"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function parseProjectDotEnv(content) {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = parsePortableDotEnv(content);
|
|
315
|
+
if (typeof nodeUtil.parseEnv === "function") {
|
|
316
|
+
const native = nodeUtil.parseEnv(content);
|
|
317
|
+
if (JSON.stringify(Object.entries(parsed).sort()) !== JSON.stringify(Object.entries(native).sort()))
|
|
318
|
+
throw new Error("ambiguous syntax");
|
|
319
|
+
}
|
|
320
|
+
return parsed;
|
|
321
|
+
} catch {
|
|
322
|
+
throw new UserError("Cannot parse project .env; contents omitted. / \u65E0\u6CD5\u89E3\u6790\u9879\u76EE .env\uFF0C\u5DF2\u9690\u85CF\u5185\u5BB9\u3002");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function parsePortableDotEnv(content) {
|
|
326
|
+
const values = /* @__PURE__ */ Object.create(null);
|
|
327
|
+
let remaining = content.replace(/\r\n?/g, "\n");
|
|
328
|
+
while (remaining.trim()) {
|
|
329
|
+
remaining = remaining.trimStart();
|
|
330
|
+
if (remaining.startsWith("#")) {
|
|
331
|
+
remaining = remaining.slice(remaining.indexOf("\n") < 0 ? remaining.length : remaining.indexOf("\n") + 1);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const assignment = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=[ \t]*/.exec(remaining);
|
|
335
|
+
if (!assignment) throw new Error("invalid assignment");
|
|
336
|
+
remaining = remaining.slice(assignment[0].length);
|
|
337
|
+
let value;
|
|
338
|
+
const delimiter = remaining[0];
|
|
339
|
+
if (["'", '"', "`"].includes(delimiter ?? "")) {
|
|
340
|
+
const end = remaining.indexOf(delimiter, 1);
|
|
341
|
+
if (end < 0) throw new Error("unterminated value");
|
|
342
|
+
value = remaining.slice(1, end);
|
|
343
|
+
if (delimiter === '"') value = value.replaceAll("\\n", "\n");
|
|
344
|
+
remaining = remaining.slice(end + 1);
|
|
345
|
+
const tail = remaining.split("\n", 1)[0];
|
|
346
|
+
if (tail.trim() && !tail.trimStart().startsWith("#")) throw new Error("trailing value");
|
|
347
|
+
} else {
|
|
348
|
+
value = remaining.split("\n", 1)[0].split("#", 1)[0].trim();
|
|
349
|
+
}
|
|
350
|
+
values[assignment[1]] = value;
|
|
351
|
+
const newline = remaining.indexOf("\n");
|
|
352
|
+
remaining = newline < 0 ? "" : remaining.slice(newline + 1);
|
|
353
|
+
}
|
|
354
|
+
return values;
|
|
355
|
+
}
|
|
356
|
+
function encodeAssignment(name, value) {
|
|
357
|
+
for (const quote of ["'", '"', "`"]) {
|
|
358
|
+
if (value.includes(quote) || value.includes("\0")) continue;
|
|
359
|
+
const assignment = `${name}=${quote}${value}${quote}
|
|
360
|
+
`;
|
|
361
|
+
try {
|
|
362
|
+
if (parseProjectDotEnv(assignment)[name] === value) return assignment;
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
throw new UserError(
|
|
367
|
+
"A Vault secret cannot be represented losslessly in .env. Supply an environment variable reference instead. / Vault \u5BC6\u94A5\u65E0\u6CD5\u65E0\u635F\u5199\u5165 .env\uFF0C\u8BF7\u6539\u7528\u73AF\u5883\u53D8\u91CF\u5F15\u7528\u3002"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
async function assertUnchanged(root, source) {
|
|
371
|
+
const path = resolve(root, source.path);
|
|
372
|
+
const details = await lstat(path);
|
|
373
|
+
if (!details.isFile() || details.isSymbolicLink() || (details.mode & 511) !== source.mode || !Buffer.from(source.content).equals(await readFile(path))) {
|
|
374
|
+
throw new UserError("Vault source changed. Preview Build again. / Vault \u6E90\u6587\u4EF6\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u6784\u5EFA\u3002");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function writePrivateAtomic(path, content, mode, beforeRename) {
|
|
378
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
379
|
+
try {
|
|
380
|
+
await writeFile(temporary, content, { mode: 384, flag: "wx" });
|
|
381
|
+
await chmod(temporary, mode);
|
|
382
|
+
await beforeRename();
|
|
383
|
+
await rename(temporary, path);
|
|
384
|
+
} finally {
|
|
385
|
+
await rm(temporary, { force: true });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function redactVaultText(text, literals) {
|
|
389
|
+
let redacted = text;
|
|
390
|
+
for (const literal of [...new Set(literals)].sort((left, right) => right.length - left.length))
|
|
391
|
+
redacted = redacted.replaceAll(literal, "[redacted]");
|
|
392
|
+
return redacted;
|
|
393
|
+
}
|
|
394
|
+
function isRecord(value) {
|
|
395
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
396
|
+
}
|
|
397
|
+
function hasCode(error, code) {
|
|
398
|
+
return !!error && typeof error === "object" && "code" in error && error.code === code;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/internal/project-workspace/index.ts
|
|
402
|
+
var PROJECT_METADATA_FILE = "project.json";
|
|
403
|
+
var PROJECT_INTERNAL_DIRECTORY = ".openagentpack";
|
|
404
|
+
var PROJECT_BUILD_FILE = ".openagentpack/build/agents.yaml";
|
|
405
|
+
var PROJECT_BUILD_MANIFEST = ".openagentpack/build/manifest.json";
|
|
406
|
+
var PROJECT_STATE_FILE = ".openagentpack/state.json";
|
|
407
|
+
var FILE_AUTO_ASSOCIATION_IGNORE_FILE = ".openagentpack-ignore";
|
|
408
|
+
var DIRECTORY_PROJECT_PROVIDER = "bailian";
|
|
409
|
+
var DIRECTORY_PROJECT_PROVIDER_CONFIG = {
|
|
410
|
+
api_key: `\${DASHSCOPE_API_KEY}`,
|
|
411
|
+
base_url: `\${BAILIAN_BASE_URL}`
|
|
412
|
+
};
|
|
413
|
+
var DIRECTORY_RESOURCE_TYPES = ["environment", "vault", "memory_store", "file"];
|
|
414
|
+
var DIRECTORY_RESOURCE_SPECS = {
|
|
415
|
+
environment: { section: "environments", directory: "environments", metadataFile: "environment.json" },
|
|
416
|
+
vault: { section: "vaults", directory: "vaults", metadataFile: "vault.json" },
|
|
417
|
+
memory_store: { section: "memory_stores", directory: "memory-stores", metadataFile: "memory-store.json" },
|
|
418
|
+
file: { section: "files", directory: "files", metadataFile: "file.json" }
|
|
419
|
+
};
|
|
420
|
+
var IGNORED_ROOT_NAMES = /* @__PURE__ */ new Set([
|
|
421
|
+
".git",
|
|
422
|
+
".openagentpack",
|
|
423
|
+
"agents.yaml",
|
|
424
|
+
"agents.state.json",
|
|
425
|
+
"node_modules",
|
|
426
|
+
"dist",
|
|
427
|
+
"build",
|
|
428
|
+
".cache",
|
|
429
|
+
".DS_Store"
|
|
430
|
+
]);
|
|
431
|
+
var IGNORED_SOURCE_NAMES = /* @__PURE__ */ new Set([".env"]);
|
|
432
|
+
var DirectoryProjectMutationConflictError = class extends UserError {
|
|
433
|
+
status = 409;
|
|
434
|
+
constructor(message) {
|
|
435
|
+
super(message);
|
|
436
|
+
this.name = "DirectoryProjectMutationConflictError";
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
async function resolveDirectoryProjectRoot(input = ".") {
|
|
440
|
+
const root = resolve2(input);
|
|
441
|
+
const details = await stat(root).catch(() => null);
|
|
442
|
+
if (!details?.isDirectory()) throw new UserError(`Project directory does not exist: ${root}`);
|
|
443
|
+
if (!await pathExists(resolve2(root, PROJECT_METADATA_FILE))) {
|
|
444
|
+
let ancestor = dirname(root);
|
|
445
|
+
while (ancestor !== root) {
|
|
446
|
+
if (await pathExists(resolve2(ancestor, PROJECT_METADATA_FILE))) {
|
|
447
|
+
const quotedRoot = `'${ancestor.replaceAll("'", "'\\''")}'`;
|
|
448
|
+
throw new UserError(
|
|
449
|
+
`Not a project root: ${root} (${PROJECT_METADATA_FILE} is missing).
|
|
450
|
+
Project root: ${ancestor}
|
|
451
|
+
Run from the project root: cd ${quotedRoot}
|
|
452
|
+
Or specify the project directory: --project ${quotedRoot}`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
const parent = dirname(ancestor);
|
|
456
|
+
if (parent === ancestor) break;
|
|
457
|
+
ancestor = parent;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return root;
|
|
461
|
+
}
|
|
462
|
+
async function inspectDirectoryProject(projectDirectory = ".") {
|
|
463
|
+
const projectRoot = await resolveDirectoryProjectRoot(projectDirectory);
|
|
464
|
+
const sourceFiles = await scanProjectSource(projectRoot);
|
|
465
|
+
return inspectSourceFiles(projectRoot, sourceFiles);
|
|
466
|
+
}
|
|
467
|
+
async function inspectSourceFiles(projectRoot, sourceFiles, metadataOverrides) {
|
|
468
|
+
const projectRevision = hashFiles(sourceFiles);
|
|
469
|
+
const diagnostics = [];
|
|
470
|
+
const warnings = [];
|
|
471
|
+
let assembled = null;
|
|
472
|
+
try {
|
|
473
|
+
assembled = await assembleProject(projectRoot, sourceFiles, metadataOverrides);
|
|
474
|
+
diagnostics.push(...assembled.diagnostics);
|
|
475
|
+
warnings.push(...assembled.warnings);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
diagnostics.push(toDiagnostic(error));
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
project_root: projectRoot,
|
|
481
|
+
project_revision: projectRevision,
|
|
482
|
+
source_manifest_hash: projectRevision,
|
|
483
|
+
canonical_yaml: assembled?.canonicalYaml ?? "",
|
|
484
|
+
yaml_hash: hash(assembled?.canonicalYaml ?? ""),
|
|
485
|
+
loaded: assembled?.loaded ?? null,
|
|
486
|
+
diagnostics,
|
|
487
|
+
warnings,
|
|
488
|
+
organization_moves: assembled?.moves ?? [],
|
|
489
|
+
source_files: sourceFiles
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
async function validateDirectoryProject(projectDirectory = ".") {
|
|
493
|
+
return inspectDirectoryProject(projectDirectory);
|
|
494
|
+
}
|
|
495
|
+
async function resolveDirectoryProjectRuntime(projectDirectory = ".") {
|
|
496
|
+
const inspection = await inspectDirectoryProject(projectDirectory);
|
|
497
|
+
const blockingDiagnostic = inspection.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
498
|
+
if (blockingDiagnostic) throw new UserError(blockingDiagnostic.message);
|
|
499
|
+
const rawConfig = parse(inspection.canonical_yaml);
|
|
500
|
+
return resolveProjectConfigFromObject(rawConfig, {
|
|
501
|
+
projectName: basename(inspection.project_root),
|
|
502
|
+
basePath: resolve2(inspection.project_root, ".openagentpack/build"),
|
|
503
|
+
resolveEnv: true,
|
|
504
|
+
environment: await readProjectEnvironment(inspection.project_root)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async function getProjectBuildStatus(projectDirectory, inspection) {
|
|
508
|
+
const projectRoot = await resolveDirectoryProjectRoot(projectDirectory);
|
|
509
|
+
const current = inspection ?? await inspectDirectoryProject(projectRoot);
|
|
510
|
+
let manifest;
|
|
511
|
+
try {
|
|
512
|
+
manifest = JSON.parse(await readFile2(resolve2(projectRoot, PROJECT_BUILD_MANIFEST), "utf8"));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (isFsError(error, "ENOENT"))
|
|
515
|
+
return { exists: false, stale: true, manifest: null, reasons: ["Project has not been built."] };
|
|
516
|
+
return { exists: false, stale: true, manifest: null, reasons: ["Build manifest is unreadable."] };
|
|
517
|
+
}
|
|
518
|
+
const reasons = [];
|
|
519
|
+
if (manifest.project_revision !== current.project_revision)
|
|
520
|
+
reasons.push("Project source changed after the last Build.");
|
|
521
|
+
if (manifest.source_manifest_hash !== current.source_manifest_hash) reasons.push("Project source manifest is stale.");
|
|
522
|
+
const builtYaml = await readFile2(resolve2(projectRoot, PROJECT_BUILD_FILE), "utf8").catch(() => null);
|
|
523
|
+
if (builtYaml === null) reasons.push("Generated agents.yaml is missing.");
|
|
524
|
+
else if (hash(builtYaml) !== manifest.yaml_hash) reasons.push("Generated agents.yaml does not match its manifest.");
|
|
525
|
+
return { exists: true, stale: reasons.length > 0, manifest, reasons };
|
|
526
|
+
}
|
|
527
|
+
async function previewProjectBuild(projectDirectory = ".") {
|
|
528
|
+
const projectRoot = await resolveDirectoryProjectRoot(projectDirectory);
|
|
529
|
+
const sourceFiles = await scanProjectSource(projectRoot);
|
|
530
|
+
let migration;
|
|
531
|
+
let migrationError;
|
|
532
|
+
try {
|
|
533
|
+
migration = await planVaultSecretMigration(projectRoot, sourceFiles);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
migrationError = toDiagnostic(error);
|
|
536
|
+
}
|
|
537
|
+
const inspection = await inspectSourceFiles(projectRoot, sourceFiles, migration?.overrides);
|
|
538
|
+
const literals = migration?.literals ?? [];
|
|
539
|
+
if (migrationError) {
|
|
540
|
+
inspection.diagnostics = [migrationError];
|
|
541
|
+
inspection.canonical_yaml = "# Vault migration could not be previewed safely.\n";
|
|
542
|
+
} else if (migration?.count) {
|
|
543
|
+
inspection.warnings.push({
|
|
544
|
+
severity: "warning",
|
|
545
|
+
code: "project.vault.secrets.externalized",
|
|
546
|
+
message: `${migration.count} Vault secret(s) will move to project .env; vault.json will use environment references. / ${migration.count} \u4E2A Vault \u5BC6\u94A5\u5C06\u79FB\u5165\u9879\u76EE .env\uFF0Cvault.json \u5C06\u6539\u7528\u73AF\u5883\u53D8\u91CF\u5F15\u7528\u3002`
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
inspection.loaded = null;
|
|
550
|
+
inspection.diagnostics = inspection.diagnostics.map((diagnostic) => ({
|
|
551
|
+
...diagnostic,
|
|
552
|
+
message: redactVaultText(diagnostic.message, literals)
|
|
553
|
+
}));
|
|
554
|
+
inspection.warnings = inspection.warnings.map((diagnostic) => ({
|
|
555
|
+
...diagnostic,
|
|
556
|
+
message: redactVaultText(diagnostic.message, literals)
|
|
557
|
+
}));
|
|
558
|
+
inspection.source_files = sourceFiles.map((file) => {
|
|
559
|
+
if (hasErrors(inspection.diagnostics) && file.path.endsWith("/vault.json"))
|
|
560
|
+
return { ...file, content: Buffer.from("[omitted]") };
|
|
561
|
+
const normalized = migration?.overrides.get(file.path);
|
|
562
|
+
if (normalized) return { ...file, content: Buffer.from(JSON.stringify(normalized, null, 2)) };
|
|
563
|
+
if (!literals.some((literal) => Buffer.from(file.content).includes(literal))) return file;
|
|
564
|
+
return { ...file, content: Buffer.from("[omitted: contains a Vault secret]") };
|
|
565
|
+
});
|
|
566
|
+
const buildStatus = await getProjectBuildStatus(inspection.project_root, inspection);
|
|
567
|
+
const beforeYaml = await readFile2(resolve2(inspection.project_root, PROJECT_BUILD_FILE), "utf8").catch(() => "");
|
|
568
|
+
const safeBefore = await inspectProjectSource(beforeYaml, resolve2(projectRoot, PROJECT_BUILD_FILE));
|
|
569
|
+
return {
|
|
570
|
+
...inspection,
|
|
571
|
+
before_yaml: safeBefore.redacted_source,
|
|
572
|
+
after_yaml: inspection.canonical_yaml,
|
|
573
|
+
build_status: buildStatus,
|
|
574
|
+
can_build: !hasErrors(inspection.diagnostics)
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function commitProjectBuild(input) {
|
|
578
|
+
return withProjectMutation(input.projectRoot, "build", async () => {
|
|
579
|
+
const before = await previewProjectBuild(input.projectRoot);
|
|
580
|
+
if (before.project_revision !== input.baseRevision)
|
|
581
|
+
throw new UserError("Project source changed. Preview Build again.");
|
|
582
|
+
if (!before.can_build) throw new UserError("Project contains errors and cannot be built.");
|
|
583
|
+
const sourceFiles = await scanProjectSource(before.project_root);
|
|
584
|
+
if (hashFiles(sourceFiles) !== input.baseRevision)
|
|
585
|
+
throw new UserError("Project source changed. Preview Build again.");
|
|
586
|
+
const migration = await planVaultSecretMigration(before.project_root, sourceFiles);
|
|
587
|
+
const assembled = await assembleProject(before.project_root, sourceFiles, migration.overrides);
|
|
588
|
+
if (hasErrors(assembled.diagnostics)) throw new UserError("Project contains errors and cannot be built.");
|
|
589
|
+
await applyVaultSecretMigration(before.project_root, sourceFiles, migration);
|
|
590
|
+
await applyProjectAutoAssociations(before.project_root, assembled.autoAssociations);
|
|
591
|
+
for (const move of before.organization_moves) await applyOrganizationMove(before.project_root, move);
|
|
592
|
+
const after = await previewProjectBuild(before.project_root);
|
|
593
|
+
if (!after.can_build) throw new UserError("Organized project contains errors and cannot be built.");
|
|
594
|
+
const materialized = await inspectDirectoryProject(before.project_root);
|
|
595
|
+
if (materialized.project_revision !== after.project_revision || materialized.yaml_hash !== after.yaml_hash) {
|
|
596
|
+
throw new UserError("Project source changed during Build. Preview Build again.");
|
|
597
|
+
}
|
|
598
|
+
const manifest = {
|
|
599
|
+
schema_version: 1,
|
|
600
|
+
project_revision: after.project_revision,
|
|
601
|
+
source_manifest_hash: after.source_manifest_hash,
|
|
602
|
+
yaml_hash: after.yaml_hash,
|
|
603
|
+
built_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
604
|
+
};
|
|
605
|
+
await writeTextAtomic(resolve2(after.project_root, PROJECT_BUILD_FILE), after.canonical_yaml, 384);
|
|
606
|
+
await writeTextAtomic(
|
|
607
|
+
resolve2(after.project_root, PROJECT_BUILD_MANIFEST),
|
|
608
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
609
|
+
`,
|
|
610
|
+
384
|
|
611
|
+
);
|
|
612
|
+
return { ...after, manifest };
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async function readValidProjectBuild(projectDirectory = ".") {
|
|
616
|
+
const inspection = await inspectDirectoryProject(projectDirectory);
|
|
617
|
+
if (hasErrors(inspection.diagnostics)) throw new UserError("Project source is invalid. Run project validate.");
|
|
618
|
+
const status = await getProjectBuildStatus(inspection.project_root, inspection);
|
|
619
|
+
if (!status.exists || status.stale || !status.manifest) {
|
|
620
|
+
throw new UserError(
|
|
621
|
+
`Project Build is missing or stale. Run project build first. ${status.reasons.join(" ")}`.trim()
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const buildPath = resolve2(inspection.project_root, PROJECT_BUILD_FILE);
|
|
625
|
+
const yaml = await readFile2(buildPath, "utf8");
|
|
626
|
+
return { inspection, manifest: status.manifest, buildPath, yaml };
|
|
627
|
+
}
|
|
628
|
+
async function planProjectPublish(projectDirectory = ".", options = {}) {
|
|
629
|
+
const build = await readValidProjectBuild(projectDirectory);
|
|
630
|
+
const loaded = await (options.resolveBuild ?? resolveProjectConfig)(build.buildPath, {
|
|
631
|
+
environment: await readProjectEnvironment(build.inspection.project_root)
|
|
632
|
+
});
|
|
633
|
+
const planned = await planProjectWithStateBackend(
|
|
634
|
+
{
|
|
635
|
+
projectName: basename(build.inspection.project_root),
|
|
636
|
+
config: loaded.config,
|
|
637
|
+
configPath: build.buildPath,
|
|
638
|
+
providers: loaded.config.providers,
|
|
639
|
+
stateBackend: new LocalFileStateBackend({
|
|
640
|
+
statePath: resolve2(build.inspection.project_root, PROJECT_STATE_FILE)
|
|
641
|
+
}),
|
|
642
|
+
stateScope: { projectId: basename(build.inspection.project_root) }
|
|
643
|
+
},
|
|
644
|
+
options
|
|
645
|
+
);
|
|
646
|
+
return {
|
|
647
|
+
project_root: build.inspection.project_root,
|
|
648
|
+
project_revision: build.inspection.project_revision,
|
|
649
|
+
build_manifest: build.manifest,
|
|
650
|
+
build_path: build.buildPath,
|
|
651
|
+
plan_fingerprint: projectPublishPlanFingerprint(planned),
|
|
652
|
+
planned
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async function executeProjectPublish(input) {
|
|
656
|
+
const versionService = createDirectoryWorkspaceVersionService(input.projectRoot);
|
|
657
|
+
const initial = await readValidProjectBuild(input.projectRoot);
|
|
658
|
+
if (initial.inspection.project_revision !== input.expectedProjectRevision || initial.manifest.yaml_hash !== input.expectedYamlHash) {
|
|
659
|
+
throw new UserError("Project or Build changed. Plan Publish again.");
|
|
660
|
+
}
|
|
661
|
+
const frozenSnapshot = snapshotFromInspection(initial.inspection);
|
|
662
|
+
const preparedVersion = await versionService.prepareVersion(frozenSnapshot);
|
|
663
|
+
let versionCommitted = false;
|
|
664
|
+
try {
|
|
665
|
+
const checked = await readValidProjectBuild(input.projectRoot);
|
|
666
|
+
if (checked.inspection.project_revision !== input.expectedProjectRevision || checked.manifest.yaml_hash !== input.expectedYamlHash) {
|
|
667
|
+
throw new UserError("Project or Build changed while Publish was starting.");
|
|
668
|
+
}
|
|
669
|
+
const loaded = await (input.resolveBuild ?? resolveProjectConfig)(checked.buildPath, {
|
|
670
|
+
environment: await readProjectEnvironment(checked.inspection.project_root)
|
|
671
|
+
});
|
|
672
|
+
const run = await writeProjectRuntime(
|
|
673
|
+
{
|
|
674
|
+
projectName: basename(checked.inspection.project_root),
|
|
675
|
+
config: loaded.config,
|
|
676
|
+
configPath: checked.buildPath,
|
|
677
|
+
providers: loaded.config.providers,
|
|
678
|
+
stateBackend: new LocalFileStateBackend({
|
|
679
|
+
statePath: resolve2(checked.inspection.project_root, PROJECT_STATE_FILE)
|
|
680
|
+
}),
|
|
681
|
+
stateScope: { projectId: basename(checked.inspection.project_root) }
|
|
682
|
+
},
|
|
683
|
+
async (context) => {
|
|
684
|
+
const planned = await planProjectContext(context, {
|
|
685
|
+
provider: input.provider,
|
|
686
|
+
refresh: input.refresh,
|
|
687
|
+
onFeedback: input.onFeedback
|
|
688
|
+
});
|
|
689
|
+
if (projectPublishPlanFingerprint(planned) !== input.expectedPlanFingerprint) {
|
|
690
|
+
throw new UserError("Project, remote resources, or Publish plan changed. Plan Publish again.");
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
planned,
|
|
694
|
+
execution: await executePlannedProject(planned, {
|
|
695
|
+
concurrency: input.concurrency,
|
|
696
|
+
policy: input.policy,
|
|
697
|
+
confirm: input.confirm,
|
|
698
|
+
onFeedback: input.onFeedback
|
|
699
|
+
})
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
const incomplete = run.execution?.results.some((result) => result.status !== "success") ?? false;
|
|
704
|
+
if (incomplete) throw new UserError("Publish incomplete: one or more remote actions did not succeed.");
|
|
705
|
+
const version = preparedVersion ? await versionService.commitPrepared(preparedVersion, "Publish project") : null;
|
|
706
|
+
versionCommitted = true;
|
|
707
|
+
const working = await inspectDirectoryProject(input.projectRoot);
|
|
708
|
+
return {
|
|
709
|
+
planned: run.planned,
|
|
710
|
+
execution: run.execution,
|
|
711
|
+
version: version ? { version_id: version.version_id, short_version: version.short_version, message: version.message } : null,
|
|
712
|
+
published_revision: frozenSnapshot.project_revision,
|
|
713
|
+
working_revision: working.project_revision,
|
|
714
|
+
working_tree_changed: working.project_revision !== frozenSnapshot.project_revision
|
|
715
|
+
};
|
|
716
|
+
} finally {
|
|
717
|
+
if (!versionCommitted) await versionService.releasePrepared(preparedVersion);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function initializeDirectoryProject(input = {}) {
|
|
721
|
+
const projectRoot = resolve2(input.projectRoot ?? ".");
|
|
722
|
+
await mkdir(projectRoot, { recursive: true });
|
|
723
|
+
const initialized = await withProjectMutation(projectRoot, "init", async () => {
|
|
724
|
+
const projectPath = resolve2(projectRoot, PROJECT_METADATA_FILE);
|
|
725
|
+
if (await pathExists(projectPath)) throw new UserError(`Directory project already exists: ${projectPath}`);
|
|
726
|
+
const yamlPath = resolve2(projectRoot, "agents.yaml");
|
|
727
|
+
let converted = false;
|
|
728
|
+
let stateMigrated = false;
|
|
729
|
+
if (await pathExists(yamlPath)) {
|
|
730
|
+
await convertYamlProject(projectRoot, yamlPath);
|
|
731
|
+
converted = true;
|
|
732
|
+
const oldState = resolve2(projectRoot, "agents.state.json");
|
|
733
|
+
if (await pathExists(oldState)) {
|
|
734
|
+
await mkdir(dirname(resolve2(projectRoot, PROJECT_STATE_FILE)), { recursive: true });
|
|
735
|
+
await cp(oldState, resolve2(projectRoot, PROJECT_STATE_FILE), { force: false });
|
|
736
|
+
stateMigrated = true;
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
await scaffoldProject(projectRoot);
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
project_root: projectRoot,
|
|
743
|
+
converted_from_yaml: converted,
|
|
744
|
+
state_migrated: stateMigrated,
|
|
745
|
+
baseline_version: null
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
const enabled = await createDirectoryWorkspaceVersionService(projectRoot).enable("Initialize project");
|
|
749
|
+
return { ...initialized, baseline_version: enabled.version?.version_id ?? enabled.versioning.head_version };
|
|
750
|
+
}
|
|
751
|
+
function createDirectoryWorkspaceVersionService(projectDirectory) {
|
|
752
|
+
const projectRoot = resolve2(projectDirectory);
|
|
753
|
+
return createDirectoryProjectVersionService({
|
|
754
|
+
projectRoot,
|
|
755
|
+
adapter: {
|
|
756
|
+
readSnapshot: async () => snapshotFromInspection(await inspectDirectoryProject(projectRoot)),
|
|
757
|
+
restoreSnapshot: async (snapshot, baseRevision) => restoreDirectorySnapshot(projectRoot, snapshot, baseRevision)
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
async function assertLegacyYamlNotShadowed(configFile) {
|
|
762
|
+
const configPath = resolve2(configFile);
|
|
763
|
+
if (basename(configPath) !== "agents.yaml") return;
|
|
764
|
+
if (await pathExists(resolve2(dirname(configPath), PROJECT_METADATA_FILE))) {
|
|
765
|
+
throw new UserError(
|
|
766
|
+
"This directory is managed as a directory project. Use `agents project build` and `agents project publish`."
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async function assembleProject(projectRoot, sourceFiles, metadataOverrides) {
|
|
771
|
+
if (!sourceFiles.some((file) => file.path === PROJECT_METADATA_FILE)) {
|
|
772
|
+
throw new UserError(`Missing ${PROJECT_METADATA_FILE}. Run project init first.`);
|
|
773
|
+
}
|
|
774
|
+
const project = await readJsonObject(resolve2(projectRoot, PROJECT_METADATA_FILE));
|
|
775
|
+
if ("providers" in project || isRecord2(project.defaults) && "provider" in project.defaults) {
|
|
776
|
+
throw new UserError(
|
|
777
|
+
`project.json cannot declare providers or defaults.provider; directory projects use '${DIRECTORY_PROJECT_PROVIDER}' automatically.`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
if ("defaults" in project && !isRecord2(project.defaults)) {
|
|
781
|
+
throw new UserError("project.json defaults must be an object.");
|
|
782
|
+
}
|
|
783
|
+
const directorySections = [
|
|
784
|
+
"agents",
|
|
785
|
+
"skills",
|
|
786
|
+
...DIRECTORY_RESOURCE_TYPES.map((type) => DIRECTORY_RESOURCE_SPECS[type].section)
|
|
787
|
+
];
|
|
788
|
+
const misplacedSections = directorySections.filter((section) => section in project);
|
|
789
|
+
if (misplacedSections.length > 0) {
|
|
790
|
+
throw new UserError(
|
|
791
|
+
`project.json cannot declare ${misplacedSections.join(", ")}; use agents/ and Agent-local or resources/ directories.`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const agents = {};
|
|
795
|
+
const agentSources = /* @__PURE__ */ new Map();
|
|
796
|
+
const autoAssociations = {
|
|
797
|
+
fileMoves: [],
|
|
798
|
+
jsonWrites: /* @__PURE__ */ new Map(),
|
|
799
|
+
warnings: []
|
|
800
|
+
};
|
|
801
|
+
const agentDirectories = await childDirectories(resolve2(projectRoot, "agents"));
|
|
802
|
+
for (const agentId of agentDirectories) {
|
|
803
|
+
const agentDirectory = resolve2(projectRoot, "agents", agentId);
|
|
804
|
+
const agentPath = resolve2(agentDirectory, "agent.json");
|
|
805
|
+
if (!await pathExists(agentPath)) continue;
|
|
806
|
+
const agentSource = await readJsonObject(agentPath);
|
|
807
|
+
agentSources.set(agentId, agentSource);
|
|
808
|
+
const agent = structuredClone(agentSource);
|
|
809
|
+
if ("id" in agent && agent.id !== agentId)
|
|
810
|
+
throw new UserError(`agents/${agentId}/agent.json: id cannot differ from its directory name.`);
|
|
811
|
+
delete agent.id;
|
|
812
|
+
if ("instructions" in agent)
|
|
813
|
+
throw new UserError(`agents/${agentId}/agent.json: instructions must be stored in instructions.md.`);
|
|
814
|
+
const instructionsPath = resolve2(agentDirectory, "instructions.md");
|
|
815
|
+
if (!await pathExists(instructionsPath)) throw new UserError(`agents/${agentId}/instructions.md is required.`);
|
|
816
|
+
agents[agentId] = {
|
|
817
|
+
...agent,
|
|
818
|
+
instructions: buildRelativePath(`agents/${agentId}/instructions.md`)
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const skills = await discoverSkills(projectRoot, agentDirectories, autoAssociations);
|
|
822
|
+
const skillById = /* @__PURE__ */ new Map();
|
|
823
|
+
for (const skill of skills) {
|
|
824
|
+
if (skillById.has(skill.id)) throw new UserError(`Duplicate local skill id: ${skill.id}`);
|
|
825
|
+
skillById.set(skill.id, skill);
|
|
826
|
+
if (skill.ownerAgent) {
|
|
827
|
+
autoAssociateSkill(skill, projectRoot, agents, agentSources, autoAssociations);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const referenceCounts = countSkillReferences(agents, skillById);
|
|
831
|
+
const moves = [];
|
|
832
|
+
for (const skill of skills) {
|
|
833
|
+
if (!skill.rootSkill && (referenceCounts.get(skill.id)?.size ?? 0) > 1) {
|
|
834
|
+
moves.push({
|
|
835
|
+
resource_type: "skill",
|
|
836
|
+
resource_id: skill.id,
|
|
837
|
+
from: skill.relativeDirectory,
|
|
838
|
+
to: `skills/${skill.id}`,
|
|
839
|
+
reason: "shared"
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const warnings = autoAssociations.warnings;
|
|
844
|
+
const skillDeclarations = {};
|
|
845
|
+
for (const skill of skills) {
|
|
846
|
+
const references = referenceCounts.get(skill.id)?.size ?? 0;
|
|
847
|
+
if (references === 0) {
|
|
848
|
+
warnings.push({
|
|
849
|
+
severity: "warning",
|
|
850
|
+
code: "project.skill.unreferenced",
|
|
851
|
+
message: `${skill.location}/skill.json: skill '${skill.id}' is not referenced and is excluded from Build.`
|
|
852
|
+
});
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const finalDirectory = moves.find((move) => move.resource_type === "skill" && move.resource_id === skill.id)?.to ?? skill.relativeDirectory;
|
|
856
|
+
const declaration = { ...skill.metadata };
|
|
857
|
+
delete declaration.id;
|
|
858
|
+
delete declaration.source;
|
|
859
|
+
skillDeclarations[skill.id] = { ...declaration, source: buildRelativePath(finalDirectory) };
|
|
860
|
+
}
|
|
861
|
+
const resources = await discoverDirectoryResources(
|
|
862
|
+
projectRoot,
|
|
863
|
+
agentDirectories,
|
|
864
|
+
autoAssociations,
|
|
865
|
+
metadataOverrides
|
|
866
|
+
);
|
|
867
|
+
const resourceKeys = /* @__PURE__ */ new Set();
|
|
868
|
+
for (const resource of resources) {
|
|
869
|
+
const key = `${resource.type}:${resource.id}`;
|
|
870
|
+
if (resourceKeys.has(key)) throw new UserError(`Duplicate ${resource.type} id: ${resource.id}`);
|
|
871
|
+
resourceKeys.add(key);
|
|
872
|
+
autoAssociateDirectoryResource(resource, resources, projectRoot, agents, agentSources, autoAssociations);
|
|
873
|
+
if (resource.owner_agent && isDirectoryResourceShared(resource.type, resource.id, resource.owner_agent, agents, project)) {
|
|
874
|
+
const spec = DIRECTORY_RESOURCE_SPECS[resource.type];
|
|
875
|
+
moves.push({
|
|
876
|
+
resource_type: resource.type,
|
|
877
|
+
resource_id: resource.id,
|
|
878
|
+
from: resource.relative_directory,
|
|
879
|
+
to: `resources/${spec.directory}/${resource.id}`,
|
|
880
|
+
reason: "shared"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const rawConfig = {
|
|
885
|
+
...project,
|
|
886
|
+
providers: { [DIRECTORY_PROJECT_PROVIDER]: DIRECTORY_PROJECT_PROVIDER_CONFIG },
|
|
887
|
+
defaults: { ...isRecord2(project.defaults) ? project.defaults : {}, provider: DIRECTORY_PROJECT_PROVIDER },
|
|
888
|
+
agents,
|
|
889
|
+
skills: skillDeclarations
|
|
890
|
+
};
|
|
891
|
+
for (const type of DIRECTORY_RESOURCE_TYPES) {
|
|
892
|
+
const declarations = {};
|
|
893
|
+
for (const resource of resources.filter((candidate) => candidate.type === type)) {
|
|
894
|
+
const finalDirectory = moves.find((move) => move.resource_type === type && move.resource_id === resource.id)?.to ?? resource.relative_directory;
|
|
895
|
+
declarations[resource.id] = directoryResourceDeclarationForBuild(projectRoot, resource, finalDirectory);
|
|
896
|
+
}
|
|
897
|
+
if (Object.keys(declarations).length > 0) rawConfig[DIRECTORY_RESOURCE_SPECS[type].section] = declarations;
|
|
898
|
+
}
|
|
899
|
+
const loaded = await resolveProjectConfigFromObject(rawConfig, {
|
|
900
|
+
projectName: basename(projectRoot),
|
|
901
|
+
basePath: resolve2(projectRoot, ".openagentpack/build")
|
|
902
|
+
});
|
|
903
|
+
const diagnostics = validateProjectConfig(loaded.config);
|
|
904
|
+
const canonicalYaml = stringify(sortObjectDeep(rawConfig), { lineWidth: 0, sortMapEntries: true });
|
|
905
|
+
return { canonicalYaml, loaded, diagnostics, warnings, moves, autoAssociations };
|
|
906
|
+
}
|
|
907
|
+
function directoryResourceDeclarationForBuild(projectRoot, resource, finalDirectory) {
|
|
908
|
+
const declaration = structuredClone(resource.metadata);
|
|
909
|
+
delete declaration.id;
|
|
910
|
+
if (resource.type !== "file" || typeof declaration.source !== "string" || /^https?:\/\//i.test(declaration.source)) {
|
|
911
|
+
return declaration;
|
|
912
|
+
}
|
|
913
|
+
if (isAbsolute(declaration.source)) {
|
|
914
|
+
throw new UserError(`${resource.relative_directory}/file.json: source must be relative to the File directory.`);
|
|
915
|
+
}
|
|
916
|
+
const sourcePath = resolve2(resource.directory, declaration.source);
|
|
917
|
+
if (!isPathInside(projectRoot, sourcePath)) {
|
|
918
|
+
throw new UserError(`${resource.relative_directory}/file.json: source escapes the project root.`);
|
|
919
|
+
}
|
|
920
|
+
const sourceRelativeToResource = relative(resource.directory, sourcePath);
|
|
921
|
+
const finalSourcePath = resolve2(projectRoot, finalDirectory, sourceRelativeToResource);
|
|
922
|
+
const projectRelative = relative(projectRoot, finalSourcePath).split(sep).join("/");
|
|
923
|
+
declaration.source = buildRelativePath(projectRelative);
|
|
924
|
+
return declaration;
|
|
925
|
+
}
|
|
926
|
+
async function locateDirectoryProjectResource(projectDirectory, type, id) {
|
|
927
|
+
const projectRoot = await resolveDirectoryProjectRoot(projectDirectory);
|
|
928
|
+
const resources = await discoverDirectoryResources(
|
|
929
|
+
projectRoot,
|
|
930
|
+
await childDirectories(resolve2(projectRoot, "agents"))
|
|
931
|
+
);
|
|
932
|
+
const matches = resources.filter((resource) => resource.type === type && resource.id === id);
|
|
933
|
+
if (matches.length > 1) throw new UserError(`Duplicate ${type} id: ${id}`);
|
|
934
|
+
const match = matches[0];
|
|
935
|
+
if (!match) return null;
|
|
936
|
+
const { metadata: _metadata, ...source } = match;
|
|
937
|
+
return source;
|
|
938
|
+
}
|
|
939
|
+
async function discoverDirectoryResources(projectRoot, agentDirectories, autoAssociations = createProjectAutoAssociationPlan(), metadataOverrides) {
|
|
940
|
+
const locations = [];
|
|
941
|
+
for (const type of DIRECTORY_RESOURCE_TYPES) {
|
|
942
|
+
const spec = DIRECTORY_RESOURCE_SPECS[type];
|
|
943
|
+
for (const resourceId of await resourceDirectories(resolve2(projectRoot, "resources", spec.directory))) {
|
|
944
|
+
locations.push({
|
|
945
|
+
type,
|
|
946
|
+
id: resourceId,
|
|
947
|
+
directory: resolve2(projectRoot, "resources", spec.directory, resourceId),
|
|
948
|
+
relativeDirectory: `resources/${spec.directory}/${resourceId}`
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
for (const agentId of agentDirectories) {
|
|
952
|
+
for (const resourceId of await resourceDirectories(resolve2(projectRoot, "agents", agentId, spec.directory))) {
|
|
953
|
+
locations.push({
|
|
954
|
+
type,
|
|
955
|
+
id: resourceId,
|
|
956
|
+
directory: resolve2(projectRoot, "agents", agentId, spec.directory, resourceId),
|
|
957
|
+
relativeDirectory: `agents/${agentId}/${spec.directory}/${resourceId}`,
|
|
958
|
+
ownerAgent: agentId
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const resources = [];
|
|
964
|
+
for (const location of locations) {
|
|
965
|
+
const metadataFile = DIRECTORY_RESOURCE_SPECS[location.type].metadataFile;
|
|
966
|
+
const metadataPath = resolve2(location.directory, metadataFile);
|
|
967
|
+
let metadata;
|
|
968
|
+
let inferred = false;
|
|
969
|
+
if (await pathExists(metadataPath)) {
|
|
970
|
+
metadata = metadataOverrides?.get(`${location.relativeDirectory}/${metadataFile}`) ?? await readJsonObject(metadataPath);
|
|
971
|
+
} else if (location.type === "file" && location.ownerAgent) {
|
|
972
|
+
if (await pathExists(resolve2(location.directory, FILE_AUTO_ASSOCIATION_IGNORE_FILE))) continue;
|
|
973
|
+
const fileNames = await directProjectFiles(location.directory);
|
|
974
|
+
if (fileNames.length === 0) continue;
|
|
975
|
+
if (fileNames.length > 1) {
|
|
976
|
+
throw new UserError(
|
|
977
|
+
`${location.relativeDirectory}/file.json is required when a File directory contains multiple files.`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
const [fileName] = fileNames;
|
|
981
|
+
metadata = { id: location.id, name: fileName, source: `./${fileName}` };
|
|
982
|
+
inferred = true;
|
|
983
|
+
planJsonWrite(autoAssociations, projectRoot, metadataPath, metadata);
|
|
984
|
+
autoAssociations.warnings.push({
|
|
985
|
+
severity: "warning",
|
|
986
|
+
code: "project.file.metadata.inferred",
|
|
987
|
+
message: `${location.relativeDirectory}/${fileName} will become File '${location.id}'.`
|
|
988
|
+
});
|
|
989
|
+
} else {
|
|
990
|
+
if (location.type === "file") continue;
|
|
991
|
+
throw new UserError(`${location.relativeDirectory}/${metadataFile} is required.`);
|
|
992
|
+
}
|
|
993
|
+
if (metadata.id !== location.id) {
|
|
994
|
+
throw new UserError(`${location.relativeDirectory}/${metadataFile}: id must be '${location.id}'.`);
|
|
995
|
+
}
|
|
996
|
+
resources.push({
|
|
997
|
+
type: location.type,
|
|
998
|
+
id: location.id,
|
|
999
|
+
path: metadataPath,
|
|
1000
|
+
directory: location.directory,
|
|
1001
|
+
relative_directory: location.relativeDirectory,
|
|
1002
|
+
...location.ownerAgent ? { owner_agent: location.ownerAgent } : {},
|
|
1003
|
+
metadata,
|
|
1004
|
+
...inferred ? { inferred: true } : {}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
for (const agentId of agentDirectories) {
|
|
1008
|
+
const filesDirectory = resolve2(projectRoot, "agents", agentId, DIRECTORY_RESOURCE_SPECS.file.directory);
|
|
1009
|
+
for (const fileName of await directProjectFiles(filesDirectory)) {
|
|
1010
|
+
const resourceId = inferFileResourceId(fileName);
|
|
1011
|
+
const relativeDirectory = `agents/${agentId}/files/${resourceId}`;
|
|
1012
|
+
const destinationDirectory = resolve2(projectRoot, relativeDirectory);
|
|
1013
|
+
const metadata = { id: resourceId, name: fileName, source: `./${fileName}` };
|
|
1014
|
+
resources.push({
|
|
1015
|
+
type: "file",
|
|
1016
|
+
id: resourceId,
|
|
1017
|
+
path: resolve2(destinationDirectory, "file.json"),
|
|
1018
|
+
directory: filesDirectory,
|
|
1019
|
+
relative_directory: relativeDirectory,
|
|
1020
|
+
owner_agent: agentId,
|
|
1021
|
+
metadata,
|
|
1022
|
+
inferred: true
|
|
1023
|
+
});
|
|
1024
|
+
autoAssociations.fileMoves.push({
|
|
1025
|
+
from: `agents/${agentId}/files/${fileName}`,
|
|
1026
|
+
to: `${relativeDirectory}/${fileName}`
|
|
1027
|
+
});
|
|
1028
|
+
planJsonWrite(autoAssociations, projectRoot, resolve2(destinationDirectory, "file.json"), metadata);
|
|
1029
|
+
autoAssociations.warnings.push({
|
|
1030
|
+
severity: "warning",
|
|
1031
|
+
code: "project.file.discovered",
|
|
1032
|
+
message: `agents/${agentId}/files/${fileName} will become File '${resourceId}' and be linked to Agent '${agentId}'.`
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return resources;
|
|
1037
|
+
}
|
|
1038
|
+
function isDirectoryResourceShared(type, id, ownerAgent, agents, project) {
|
|
1039
|
+
for (const [agentId, agent] of Object.entries(agents)) {
|
|
1040
|
+
if (agentId !== ownerAgent && agentReferencesDirectoryResource(agent, type, id)) return true;
|
|
1041
|
+
}
|
|
1042
|
+
const deployments = isRecord2(project.deployments) ? project.deployments : {};
|
|
1043
|
+
for (const deployment of Object.values(deployments)) {
|
|
1044
|
+
if (isRecord2(deployment) && deploymentReferencesDirectoryResource(deployment, type, id)) return true;
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
function agentReferencesDirectoryResource(agent, type, id) {
|
|
1049
|
+
if (type === "environment") return agent.environment === id;
|
|
1050
|
+
if (type === "vault") return agent.vault === id;
|
|
1051
|
+
if (type === "memory_store") return Array.isArray(agent.memory_stores) && agent.memory_stores.includes(id);
|
|
1052
|
+
if (type === "file") {
|
|
1053
|
+
return Array.isArray(agent.files) && agent.files.some((file) => file === id || isRecord2(file) && file.file === id);
|
|
1054
|
+
}
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
function deploymentReferencesDirectoryResource(deployment, type, id) {
|
|
1058
|
+
if (type === "environment") return deployment.environment === id;
|
|
1059
|
+
if (type === "vault") return Array.isArray(deployment.vaults) && deployment.vaults.includes(id);
|
|
1060
|
+
if (type !== "memory_store") return false;
|
|
1061
|
+
if (Array.isArray(deployment.memory_stores) && deployment.memory_stores.includes(id)) return true;
|
|
1062
|
+
return Array.isArray(deployment.resources) && deployment.resources.some(
|
|
1063
|
+
(resource) => isRecord2(resource) && resource.type === "memory_store" && resource.memory_store === id
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
async function discoverSkills(projectRoot, agentDirectories, autoAssociations) {
|
|
1067
|
+
const locations = [];
|
|
1068
|
+
for (const skillDirectory of await resourceDirectories(resolve2(projectRoot, "skills"))) {
|
|
1069
|
+
locations.push({
|
|
1070
|
+
path: resolve2(projectRoot, "skills", skillDirectory),
|
|
1071
|
+
relativeDirectory: `skills/${skillDirectory}`,
|
|
1072
|
+
rootSkill: true
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
for (const agentId of agentDirectories) {
|
|
1076
|
+
for (const skillDirectory of await resourceDirectories(resolve2(projectRoot, "agents", agentId, "skills"))) {
|
|
1077
|
+
locations.push({
|
|
1078
|
+
path: resolve2(projectRoot, "agents", agentId, "skills", skillDirectory),
|
|
1079
|
+
relativeDirectory: `agents/${agentId}/skills/${skillDirectory}`,
|
|
1080
|
+
rootSkill: false,
|
|
1081
|
+
ownerAgent: agentId
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const result = [];
|
|
1086
|
+
for (const location of locations) {
|
|
1087
|
+
const metadataPath = resolve2(location.path, "skill.json");
|
|
1088
|
+
let metadata;
|
|
1089
|
+
let inferred = false;
|
|
1090
|
+
if (await pathExists(metadataPath)) {
|
|
1091
|
+
metadata = await readJsonObject(metadataPath);
|
|
1092
|
+
} else if (location.ownerAgent && await pathExists(resolve2(location.path, "SKILL.md"))) {
|
|
1093
|
+
const skillId = basename(location.path);
|
|
1094
|
+
metadata = { id: skillId };
|
|
1095
|
+
inferred = true;
|
|
1096
|
+
planJsonWrite(autoAssociations, projectRoot, metadataPath, metadata);
|
|
1097
|
+
autoAssociations.warnings.push({
|
|
1098
|
+
severity: "warning",
|
|
1099
|
+
code: "project.skill.metadata.inferred",
|
|
1100
|
+
message: `${location.relativeDirectory}/skill.json will be created for Skill '${skillId}'.`
|
|
1101
|
+
});
|
|
1102
|
+
} else {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof metadata.id !== "string" || !metadata.id.trim())
|
|
1106
|
+
throw new UserError(`${relative(projectRoot, metadataPath)}: id is required.`);
|
|
1107
|
+
if (!await pathExists(resolve2(location.path, "SKILL.md")))
|
|
1108
|
+
throw new UserError(`${location.relativeDirectory}/SKILL.md is required.`);
|
|
1109
|
+
result.push({
|
|
1110
|
+
id: metadata.id,
|
|
1111
|
+
metadata,
|
|
1112
|
+
location: location.relativeDirectory,
|
|
1113
|
+
relativeDirectory: location.relativeDirectory,
|
|
1114
|
+
rootSkill: location.rootSkill,
|
|
1115
|
+
...location.ownerAgent ? { ownerAgent: location.ownerAgent } : {},
|
|
1116
|
+
...inferred ? { inferred: true } : {}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
function autoAssociateSkill(skill, projectRoot, agents, agentSources, autoAssociations) {
|
|
1122
|
+
const ownerAgent = skill.ownerAgent;
|
|
1123
|
+
if (!ownerAgent) return;
|
|
1124
|
+
const agent = agents[ownerAgent];
|
|
1125
|
+
const agentSource = agentSources.get(ownerAgent);
|
|
1126
|
+
if (!agent || !agentSource) return;
|
|
1127
|
+
if (agent.skills !== void 0 && !Array.isArray(agent.skills)) return;
|
|
1128
|
+
const references = Array.isArray(agent.skills) ? agent.skills : [];
|
|
1129
|
+
if (references.some((reference) => skillReferenceId(reference) === skill.id)) return;
|
|
1130
|
+
const nextReferences = [...references, skill.id];
|
|
1131
|
+
agent.skills = nextReferences;
|
|
1132
|
+
agentSource.skills = structuredClone(nextReferences);
|
|
1133
|
+
planJsonWrite(autoAssociations, projectRoot, resolve2(projectRoot, "agents", ownerAgent, "agent.json"), agentSource);
|
|
1134
|
+
autoAssociations.warnings.push({
|
|
1135
|
+
severity: "warning",
|
|
1136
|
+
code: "project.skill.agent_link.inferred",
|
|
1137
|
+
message: `Skill '${skill.id}' will be added to agents/${ownerAgent}/agent.json.`
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
function autoAssociateDirectoryResource(resource, resources, projectRoot, agents, agentSources, autoAssociations) {
|
|
1141
|
+
if (resource.type === "file") {
|
|
1142
|
+
autoAssociateFile(resource, projectRoot, agents, agentSources, autoAssociations);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const ownerAgent = resource.owner_agent;
|
|
1146
|
+
if (!ownerAgent) return;
|
|
1147
|
+
const agent = agents[ownerAgent];
|
|
1148
|
+
const agentSource = agentSources.get(ownerAgent);
|
|
1149
|
+
if (!agent || !agentSource) return;
|
|
1150
|
+
const agentPath = `agents/${ownerAgent}/agent.json`;
|
|
1151
|
+
if (resource.type === "memory_store") {
|
|
1152
|
+
if (agent.memory_stores !== void 0 && !Array.isArray(agent.memory_stores)) return;
|
|
1153
|
+
const references = Array.isArray(agent.memory_stores) ? agent.memory_stores : [];
|
|
1154
|
+
if (references.includes(resource.id)) return;
|
|
1155
|
+
agent.memory_stores = [...references, resource.id];
|
|
1156
|
+
agentSource.memory_stores = structuredClone(agent.memory_stores);
|
|
1157
|
+
} else {
|
|
1158
|
+
if (agent[resource.type] !== void 0) return;
|
|
1159
|
+
const candidates = resources.filter(
|
|
1160
|
+
(candidate) => candidate.owner_agent === ownerAgent && candidate.type === resource.type
|
|
1161
|
+
);
|
|
1162
|
+
if (candidates.length > 1) {
|
|
1163
|
+
throw new UserError(
|
|
1164
|
+
`${agentPath}: multiple local ${resource.type} resources (${candidates.map((candidate) => candidate.id).join(", ")}). Set '${resource.type}' explicitly to choose one.`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
agent[resource.type] = resource.id;
|
|
1168
|
+
agentSource[resource.type] = resource.id;
|
|
1169
|
+
}
|
|
1170
|
+
planJsonWrite(autoAssociations, projectRoot, resolve2(projectRoot, agentPath), agentSource);
|
|
1171
|
+
autoAssociations.warnings.push({
|
|
1172
|
+
severity: "warning",
|
|
1173
|
+
code: `project.${resource.type}.agent_link.inferred`,
|
|
1174
|
+
message: `${resource.type} '${resource.id}' will be added to ${agentPath}.`
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function autoAssociateFile(resource, projectRoot, agents, agentSources, autoAssociations) {
|
|
1178
|
+
const ownerAgent = resource.owner_agent;
|
|
1179
|
+
if (!ownerAgent) return;
|
|
1180
|
+
const agent = agents[ownerAgent];
|
|
1181
|
+
const agentSource = agentSources.get(ownerAgent);
|
|
1182
|
+
if (!agent || !agentSource) return;
|
|
1183
|
+
if (agent.files !== void 0 && !Array.isArray(agent.files)) return;
|
|
1184
|
+
const references = Array.isArray(agent.files) ? agent.files : [];
|
|
1185
|
+
if (references.some((reference) => reference === resource.id || isRecord2(reference) && reference.file === resource.id))
|
|
1186
|
+
return;
|
|
1187
|
+
const mountPath = defaultFileMountPath(resource);
|
|
1188
|
+
const nextReferences = [...references, { file: resource.id, mount_path: mountPath }];
|
|
1189
|
+
agent.files = nextReferences;
|
|
1190
|
+
agentSource.files = structuredClone(nextReferences);
|
|
1191
|
+
planJsonWrite(autoAssociations, projectRoot, resolve2(projectRoot, "agents", ownerAgent, "agent.json"), agentSource);
|
|
1192
|
+
autoAssociations.warnings.push({
|
|
1193
|
+
severity: "warning",
|
|
1194
|
+
code: "project.file.agent_link.inferred",
|
|
1195
|
+
message: `File '${resource.id}' will be mounted at '${mountPath}' in agents/${ownerAgent}/agent.json.`
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
function skillReferenceId(reference) {
|
|
1199
|
+
if (typeof reference === "string") return reference;
|
|
1200
|
+
return isRecord2(reference) && typeof reference.skill_id === "string" ? reference.skill_id : null;
|
|
1201
|
+
}
|
|
1202
|
+
function defaultFileMountPath(resource) {
|
|
1203
|
+
const source = resource.metadata.source;
|
|
1204
|
+
let fileName = resource.id;
|
|
1205
|
+
if (typeof source === "string" && /^https?:\/\//i.test(source)) {
|
|
1206
|
+
try {
|
|
1207
|
+
fileName = basename(new URL(source).pathname) || resource.id;
|
|
1208
|
+
} catch {
|
|
1209
|
+
fileName = resource.id;
|
|
1210
|
+
}
|
|
1211
|
+
} else if (typeof source === "string") {
|
|
1212
|
+
fileName = basename(source) || resource.id;
|
|
1213
|
+
}
|
|
1214
|
+
return `/mnt/${fileName}`;
|
|
1215
|
+
}
|
|
1216
|
+
function countSkillReferences(agents, skills) {
|
|
1217
|
+
const result = /* @__PURE__ */ new Map();
|
|
1218
|
+
for (const [agentId, agent] of Object.entries(agents)) {
|
|
1219
|
+
const refs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
1220
|
+
for (const ref of refs) {
|
|
1221
|
+
const id = skillReferenceId(ref);
|
|
1222
|
+
if (!id || !skills.has(id)) continue;
|
|
1223
|
+
const agentsForSkill = result.get(id) ?? /* @__PURE__ */ new Set();
|
|
1224
|
+
agentsForSkill.add(agentId);
|
|
1225
|
+
result.set(id, agentsForSkill);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return result;
|
|
1229
|
+
}
|
|
1230
|
+
async function scanProjectSource(projectRoot) {
|
|
1231
|
+
const rootReal = await realpath(projectRoot);
|
|
1232
|
+
const files = [];
|
|
1233
|
+
async function walk(directory, relativeDirectory) {
|
|
1234
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1235
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1236
|
+
if (IGNORED_SOURCE_NAMES.has(entry.name) || !relativeDirectory && IGNORED_ROOT_NAMES.has(entry.name)) continue;
|
|
1237
|
+
const absolute = resolve2(directory, entry.name);
|
|
1238
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
1239
|
+
const details = await lstat2(absolute);
|
|
1240
|
+
if (details.isSymbolicLink()) {
|
|
1241
|
+
const target = await realpath(absolute);
|
|
1242
|
+
if (target !== rootReal && !target.startsWith(`${rootReal}${sep}`))
|
|
1243
|
+
throw new UserError(`${relativePath}: symlink escapes the project root.`);
|
|
1244
|
+
throw new UserError(`${relativePath}: symlinks are not supported in directory projects.`);
|
|
1245
|
+
}
|
|
1246
|
+
if (details.isDirectory()) await walk(absolute, relativePath);
|
|
1247
|
+
else if (details.isFile())
|
|
1248
|
+
files.push({
|
|
1249
|
+
path: relativePath,
|
|
1250
|
+
mode: details.mode & 511,
|
|
1251
|
+
content: new Uint8Array(await readFile2(absolute))
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
await walk(projectRoot, "");
|
|
1256
|
+
return files;
|
|
1257
|
+
}
|
|
1258
|
+
async function restoreDirectorySnapshot(projectRoot, snapshot, baseRevision) {
|
|
1259
|
+
const current = await inspectDirectoryProject(projectRoot);
|
|
1260
|
+
if (current.project_revision !== baseRevision) throw new UserError("Project source changed before Restore.");
|
|
1261
|
+
const restoreRoot = resolve2(projectRoot, PROJECT_INTERNAL_DIRECTORY, `restore-${randomUUID2()}`);
|
|
1262
|
+
const historicalRoot = resolve2(restoreRoot, "historical");
|
|
1263
|
+
const backupRoot = resolve2(restoreRoot, "backup");
|
|
1264
|
+
const restoredPaths = [];
|
|
1265
|
+
let mutationStarted = false;
|
|
1266
|
+
await mkdir(restoreRoot, { recursive: true });
|
|
1267
|
+
try {
|
|
1268
|
+
await stageRestoreFiles(historicalRoot, snapshot.files, true);
|
|
1269
|
+
await stageRestoreFiles(backupRoot, current.source_files, false);
|
|
1270
|
+
const checked = await inspectDirectoryProject(projectRoot);
|
|
1271
|
+
if (checked.project_revision !== baseRevision) throw new UserError("Project source changed before Restore.");
|
|
1272
|
+
mutationStarted = true;
|
|
1273
|
+
const currentPaths = new Set(current.source_files.map((file) => file.path));
|
|
1274
|
+
for (const path of currentPaths) await rm2(resolve2(projectRoot, path), { force: true });
|
|
1275
|
+
await removeEmptyProjectDirectories(projectRoot, currentPaths);
|
|
1276
|
+
for (const file of snapshot.files) {
|
|
1277
|
+
const source = resolve2(historicalRoot, file.path);
|
|
1278
|
+
const destination = resolve2(projectRoot, file.path);
|
|
1279
|
+
await removeEmptyRestoreDestination(destination, file.path);
|
|
1280
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1281
|
+
await rename2(source, destination);
|
|
1282
|
+
restoredPaths.push(file.path);
|
|
1283
|
+
await chmod2(destination, file.mode);
|
|
1284
|
+
}
|
|
1285
|
+
await rm2(resolve2(projectRoot, ".openagentpack/build"), { recursive: true, force: true });
|
|
1286
|
+
} catch (restoreError) {
|
|
1287
|
+
if (mutationStarted) {
|
|
1288
|
+
try {
|
|
1289
|
+
await rollbackDirectoryRestore(projectRoot, backupRoot, current.source_files, restoredPaths);
|
|
1290
|
+
} catch (rollbackError) {
|
|
1291
|
+
throw new UserError(
|
|
1292
|
+
`Restore failed and rollback was incomplete. Restore error: ${errorMessage(restoreError)}. Rollback error: ${errorMessage(rollbackError)}.`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
throw restoreError;
|
|
1297
|
+
} finally {
|
|
1298
|
+
await rm2(restoreRoot, { recursive: true, force: true });
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function stageRestoreFiles(stagingRoot, files, rejectIgnoredFiles) {
|
|
1302
|
+
for (const file of files) {
|
|
1303
|
+
assertSafeRelative(file.path);
|
|
1304
|
+
if (rejectIgnoredFiles && file.path.split("/").some((part) => IGNORED_SOURCE_NAMES.has(part))) {
|
|
1305
|
+
throw new UserError(`Project snapshot contains an ignored source file: ${file.path}`);
|
|
1306
|
+
}
|
|
1307
|
+
const destination = resolve2(stagingRoot, file.path);
|
|
1308
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1309
|
+
await writeFile2(destination, file.content, { mode: file.mode });
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
async function rollbackDirectoryRestore(projectRoot, backupRoot, currentFiles, restoredPaths) {
|
|
1313
|
+
for (const path of restoredPaths) await rm2(resolve2(projectRoot, path), { force: true });
|
|
1314
|
+
await removeEmptyProjectDirectories(projectRoot, restoredPaths);
|
|
1315
|
+
for (const file of currentFiles) {
|
|
1316
|
+
const source = resolve2(backupRoot, file.path);
|
|
1317
|
+
const destination = resolve2(projectRoot, file.path);
|
|
1318
|
+
await clearRollbackDestination(destination);
|
|
1319
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1320
|
+
await rename2(source, destination);
|
|
1321
|
+
await chmod2(destination, file.mode);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function removeEmptyRestoreDestination(destination, relativePath) {
|
|
1325
|
+
let details;
|
|
1326
|
+
try {
|
|
1327
|
+
details = await lstat2(destination);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (isFsError(error, "ENOENT")) return;
|
|
1330
|
+
throw error;
|
|
1331
|
+
}
|
|
1332
|
+
if (!details.isDirectory()) {
|
|
1333
|
+
throw new UserError(`Cannot restore ${relativePath}: destination is occupied by an unversioned file.`);
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
await rmdir(destination);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
if (isFsError(error, "ENOTEMPTY") || isFsError(error, "EEXIST")) {
|
|
1339
|
+
throw new UserError(`Cannot restore ${relativePath}: destination directory contains unversioned files.`);
|
|
1340
|
+
}
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
async function clearRollbackDestination(destination) {
|
|
1345
|
+
let details;
|
|
1346
|
+
try {
|
|
1347
|
+
details = await lstat2(destination);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
if (isFsError(error, "ENOENT")) return;
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
if (details.isDirectory()) await rmdir(destination);
|
|
1353
|
+
else await rm2(destination, { force: true });
|
|
1354
|
+
}
|
|
1355
|
+
async function removeEmptyProjectDirectories(projectRoot, paths) {
|
|
1356
|
+
const directories = /* @__PURE__ */ new Set();
|
|
1357
|
+
for (const path of paths) {
|
|
1358
|
+
let directory = dirname(path);
|
|
1359
|
+
while (directory !== ".") {
|
|
1360
|
+
directories.add(directory);
|
|
1361
|
+
directory = dirname(directory);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
for (const directory of [...directories].sort((left, right) => right.split("/").length - left.split("/").length)) {
|
|
1365
|
+
try {
|
|
1366
|
+
await rmdir(resolve2(projectRoot, directory));
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (!isFsError(error, "ENOENT") && !isFsError(error, "ENOTEMPTY") && !isFsError(error, "EEXIST")) throw error;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function snapshotFromInspection(inspection) {
|
|
1373
|
+
return {
|
|
1374
|
+
project_revision: inspection.project_revision,
|
|
1375
|
+
canonical_yaml: inspection.canonical_yaml,
|
|
1376
|
+
files: inspection.source_files.map((file) => ({ ...file }))
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
async function applyProjectAutoAssociations(projectRoot, autoAssociations) {
|
|
1380
|
+
for (const move of autoAssociations.fileMoves) {
|
|
1381
|
+
const source = resolve2(projectRoot, move.from);
|
|
1382
|
+
const destination = resolve2(projectRoot, move.to);
|
|
1383
|
+
if (await pathExists(destination)) {
|
|
1384
|
+
throw new UserError(`Cannot organize discovered File: ${move.to} already exists.`);
|
|
1385
|
+
}
|
|
1386
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1387
|
+
await rename2(source, destination);
|
|
1388
|
+
}
|
|
1389
|
+
for (const [relativePath, value] of autoAssociations.jsonWrites) {
|
|
1390
|
+
const destination = resolve2(projectRoot, relativePath);
|
|
1391
|
+
const mode = await stat(destination).then((details) => details.mode & 511).catch((error) => {
|
|
1392
|
+
if (isFsError(error, "ENOENT")) return 420;
|
|
1393
|
+
throw error;
|
|
1394
|
+
});
|
|
1395
|
+
await writeTextAtomic(destination, `${JSON.stringify(value, null, 2)}
|
|
1396
|
+
`, mode);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
async function applyOrganizationMove(projectRoot, move) {
|
|
1400
|
+
const source = resolve2(projectRoot, move.from);
|
|
1401
|
+
const destination = resolve2(projectRoot, move.to);
|
|
1402
|
+
if (await pathExists(destination))
|
|
1403
|
+
throw new UserError(`Cannot move shared ${move.resource_type} '${move.resource_id}': ${move.to} already exists.`);
|
|
1404
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
1405
|
+
await rename2(source, destination);
|
|
1406
|
+
}
|
|
1407
|
+
async function scaffoldProject(projectRoot) {
|
|
1408
|
+
const files = directoryProjectScaffold();
|
|
1409
|
+
await scanProjectSource(projectRoot);
|
|
1410
|
+
for (const path of Object.keys(files)) {
|
|
1411
|
+
const existing = await lstat2(resolve2(projectRoot, path)).catch((error) => {
|
|
1412
|
+
if (isFsError(error, "ENOENT")) return null;
|
|
1413
|
+
throw error;
|
|
1414
|
+
});
|
|
1415
|
+
if (existing)
|
|
1416
|
+
throw new UserError(`Cannot initialize project: ${path} already exists. / \u521D\u59CB\u5316\u88AB\u963B\u6B62\uFF1A\u6587\u4EF6\u5DF2\u5B58\u5728\u3002`);
|
|
1417
|
+
}
|
|
1418
|
+
for (const [path, content] of Object.entries(files)) await writeTextAtomic(resolve2(projectRoot, path), content);
|
|
1419
|
+
}
|
|
1420
|
+
async function convertYamlProject(projectRoot, yamlPath) {
|
|
1421
|
+
const source = await readFile2(yamlPath, "utf8");
|
|
1422
|
+
const inspection = await inspectProjectSource(source, yamlPath);
|
|
1423
|
+
const blockingDiagnostic = inspection.diagnostics.find((diagnostic) => diagnostic.severity === "error");
|
|
1424
|
+
if (blockingDiagnostic) throw new UserError(blockingDiagnostic.message);
|
|
1425
|
+
const raw = parse(source);
|
|
1426
|
+
if (!isRecord2(raw)) throw new UserError("agents.yaml must contain an object.");
|
|
1427
|
+
const configuredProviders = isRecord2(raw.providers) ? Object.keys(raw.providers) : [];
|
|
1428
|
+
const defaultProvider = isRecord2(raw.defaults) && typeof raw.defaults.provider === "string" ? raw.defaults.provider : null;
|
|
1429
|
+
const unsupportedProviders = [
|
|
1430
|
+
.../* @__PURE__ */ new Set([...configuredProviders, ...defaultProvider ? [defaultProvider] : []])
|
|
1431
|
+
].filter((provider) => provider !== DIRECTORY_PROJECT_PROVIDER);
|
|
1432
|
+
if (unsupportedProviders.length > 0) {
|
|
1433
|
+
throw new UserError(
|
|
1434
|
+
`Directory projects use '${DIRECTORY_PROJECT_PROVIDER}' automatically and cannot convert providers: ${unsupportedProviders.join(", ")}.`
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
const agents = isRecord2(raw.agents) ? raw.agents : {};
|
|
1438
|
+
const skills = isRecord2(raw.skills) ? raw.skills : {};
|
|
1439
|
+
const directoryResources = Object.fromEntries(
|
|
1440
|
+
DIRECTORY_RESOURCE_TYPES.map((type) => {
|
|
1441
|
+
const section = DIRECTORY_RESOURCE_SPECS[type].section;
|
|
1442
|
+
return [type, isRecord2(raw[section]) ? raw[section] : {}];
|
|
1443
|
+
})
|
|
1444
|
+
);
|
|
1445
|
+
const project = { ...raw };
|
|
1446
|
+
delete project.providers;
|
|
1447
|
+
if (isRecord2(project.defaults)) {
|
|
1448
|
+
const defaults = { ...project.defaults };
|
|
1449
|
+
delete defaults.provider;
|
|
1450
|
+
if (Object.keys(defaults).length > 0) project.defaults = defaults;
|
|
1451
|
+
else delete project.defaults;
|
|
1452
|
+
}
|
|
1453
|
+
delete project.agents;
|
|
1454
|
+
delete project.skills;
|
|
1455
|
+
for (const type of DIRECTORY_RESOURCE_TYPES) delete project[DIRECTORY_RESOURCE_SPECS[type].section];
|
|
1456
|
+
await writeTextAtomic(resolve2(projectRoot, PROJECT_METADATA_FILE), `${JSON.stringify(project, null, 2)}
|
|
1457
|
+
`);
|
|
1458
|
+
const references = /* @__PURE__ */ new Map();
|
|
1459
|
+
for (const [agentId, value] of Object.entries(agents)) {
|
|
1460
|
+
if (!isRecord2(value)) throw new UserError(`agents.${agentId} must be an object.`);
|
|
1461
|
+
const agent = { ...value };
|
|
1462
|
+
const instructions = await materializeText(projectRoot, agent.instructions, "You are a helpful assistant.\n");
|
|
1463
|
+
delete agent.instructions;
|
|
1464
|
+
for (const ref of Array.isArray(agent.skills) ? agent.skills : []) {
|
|
1465
|
+
const skillId = typeof ref === "string" ? ref : isRecord2(ref) && typeof ref.skill_id === "string" ? ref.skill_id : null;
|
|
1466
|
+
if (!skillId) continue;
|
|
1467
|
+
const set = references.get(skillId) ?? /* @__PURE__ */ new Set();
|
|
1468
|
+
set.add(agentId);
|
|
1469
|
+
references.set(skillId, set);
|
|
1470
|
+
}
|
|
1471
|
+
const directory = resolve2(projectRoot, "agents", agentId);
|
|
1472
|
+
await mkdir(directory, { recursive: true });
|
|
1473
|
+
await writeTextAtomic(resolve2(directory, "agent.json"), `${JSON.stringify(agent, null, 2)}
|
|
1474
|
+
`);
|
|
1475
|
+
await writeTextAtomic(resolve2(directory, "instructions.md"), instructions);
|
|
1476
|
+
}
|
|
1477
|
+
for (const [skillId, value] of Object.entries(skills)) {
|
|
1478
|
+
if (!isRecord2(value)) throw new UserError(`skills.${skillId} must be an object.`);
|
|
1479
|
+
const owners = references.get(skillId) ?? /* @__PURE__ */ new Set();
|
|
1480
|
+
const relativeDirectory = owners.size === 1 ? `agents/${[...owners][0]}/skills/${skillId}` : `skills/${skillId}`;
|
|
1481
|
+
const destination = resolve2(projectRoot, relativeDirectory);
|
|
1482
|
+
await mkdir(destination, { recursive: true });
|
|
1483
|
+
const metadata = { id: skillId, ...value };
|
|
1484
|
+
delete metadata.source;
|
|
1485
|
+
await writeTextAtomic(resolve2(destination, "skill.json"), `${JSON.stringify(metadata, null, 2)}
|
|
1486
|
+
`);
|
|
1487
|
+
await materializeSkillSource(projectRoot, value.source, destination);
|
|
1488
|
+
}
|
|
1489
|
+
for (const type of DIRECTORY_RESOURCE_TYPES) {
|
|
1490
|
+
const spec = DIRECTORY_RESOURCE_SPECS[type];
|
|
1491
|
+
for (const [resourceId, value] of Object.entries(directoryResources[type])) {
|
|
1492
|
+
if (!isRecord2(value)) throw new UserError(`${spec.section}.${resourceId} must be an object.`);
|
|
1493
|
+
const ownerAgent = convertedResourceOwner(type, resourceId, agents, project);
|
|
1494
|
+
const relativeDirectory = ownerAgent ? `agents/${ownerAgent}/${spec.directory}/${resourceId}` : `resources/${spec.directory}/${resourceId}`;
|
|
1495
|
+
const destination = resolve2(projectRoot, relativeDirectory);
|
|
1496
|
+
await mkdir(destination, { recursive: true });
|
|
1497
|
+
const metadata = { id: resourceId, ...value };
|
|
1498
|
+
if (type === "file") await materializeFileSource(projectRoot, metadata, destination);
|
|
1499
|
+
await writeTextAtomic(resolve2(destination, spec.metadataFile), `${JSON.stringify(metadata, null, 2)}
|
|
1500
|
+
`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function convertedResourceOwner(type, id, agents, project) {
|
|
1505
|
+
const owners = Object.entries(agents).filter(([, agent]) => isRecord2(agent) && agentReferencesDirectoryResource(agent, type, id)).map(([agentId]) => agentId);
|
|
1506
|
+
if (owners.length !== 1) return void 0;
|
|
1507
|
+
const deployments = isRecord2(project.deployments) ? project.deployments : {};
|
|
1508
|
+
if (Object.values(deployments).some(
|
|
1509
|
+
(deployment) => isRecord2(deployment) && deploymentReferencesDirectoryResource(deployment, type, id)
|
|
1510
|
+
)) {
|
|
1511
|
+
return void 0;
|
|
1512
|
+
}
|
|
1513
|
+
return owners[0];
|
|
1514
|
+
}
|
|
1515
|
+
async function materializeText(projectRoot, value, fallback) {
|
|
1516
|
+
if (typeof value !== "string") return fallback;
|
|
1517
|
+
if (value.startsWith("./") || value.startsWith("../") || isAbsolute(value)) {
|
|
1518
|
+
return readFile2(await resolveProjectOwnedSource(projectRoot, value, "Agent instructions"), "utf8");
|
|
1519
|
+
}
|
|
1520
|
+
return value.endsWith("\n") ? value : `${value}
|
|
1521
|
+
`;
|
|
1522
|
+
}
|
|
1523
|
+
async function materializeSkillSource(projectRoot, source, destination) {
|
|
1524
|
+
if (typeof source !== "string" || /^https?:\/\//i.test(source)) {
|
|
1525
|
+
await writeTextAtomic(resolve2(destination, "SKILL.md"), "# Skill\n");
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const sourcePath = await resolveProjectOwnedSource(projectRoot, source, "Skill source");
|
|
1529
|
+
const details = await stat(sourcePath);
|
|
1530
|
+
if (details.isDirectory()) {
|
|
1531
|
+
if (sourcePath === destination) return;
|
|
1532
|
+
for (const entry of await readdir(sourcePath)) {
|
|
1533
|
+
if (entry === "skill.json") continue;
|
|
1534
|
+
await cp(resolve2(sourcePath, entry), resolve2(destination, entry), { recursive: true, errorOnExist: true });
|
|
1535
|
+
}
|
|
1536
|
+
} else {
|
|
1537
|
+
await cp(sourcePath, resolve2(destination, "SKILL.md"), { errorOnExist: true });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
async function materializeFileSource(projectRoot, metadata, destination) {
|
|
1541
|
+
const source = metadata.source;
|
|
1542
|
+
if (typeof source !== "string" || /^https?:\/\//i.test(source)) return;
|
|
1543
|
+
const sourcePath = await resolveProjectOwnedSource(projectRoot, source, "File source");
|
|
1544
|
+
const details = await stat(sourcePath);
|
|
1545
|
+
if (!details.isFile()) throw new UserError("File source must reference a file.");
|
|
1546
|
+
const originalName = basename(sourcePath);
|
|
1547
|
+
const fileName = originalName === "file.json" ? "content-file.json" : originalName;
|
|
1548
|
+
const destinationPath = resolve2(destination, fileName);
|
|
1549
|
+
if (sourcePath !== destinationPath) await cp(sourcePath, destinationPath, { errorOnExist: true });
|
|
1550
|
+
metadata.source = `./${fileName}`;
|
|
1551
|
+
}
|
|
1552
|
+
async function resolveProjectOwnedSource(projectRoot, source, label) {
|
|
1553
|
+
if (isAbsolute(source)) throw new UserError(`${label} must be a relative path inside the project root.`);
|
|
1554
|
+
const normalizedRoot = resolve2(projectRoot);
|
|
1555
|
+
const sourcePath = resolve2(normalizedRoot, source);
|
|
1556
|
+
if (!isPathInside(normalizedRoot, sourcePath)) {
|
|
1557
|
+
throw new UserError(`${label} escapes the project root.`);
|
|
1558
|
+
}
|
|
1559
|
+
const [rootRealPath, sourceRealPath] = await Promise.all([realpath(normalizedRoot), realpath(sourcePath)]);
|
|
1560
|
+
if (!isPathInside(rootRealPath, sourceRealPath)) {
|
|
1561
|
+
throw new UserError(`${label} resolves outside the project root.`);
|
|
1562
|
+
}
|
|
1563
|
+
return sourcePath;
|
|
1564
|
+
}
|
|
1565
|
+
function isPathInside(root, candidate) {
|
|
1566
|
+
const relativePath = relative(root, candidate);
|
|
1567
|
+
return Boolean(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
1568
|
+
}
|
|
1569
|
+
async function childDirectories(path) {
|
|
1570
|
+
try {
|
|
1571
|
+
return (await readdir(path, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
if (isFsError(error, "ENOENT")) return [];
|
|
1574
|
+
throw error;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function resourceDirectories(path) {
|
|
1578
|
+
return (await childDirectories(path)).filter((directory) => directory !== RESOURCE_EXAMPLES_DIRECTORY);
|
|
1579
|
+
}
|
|
1580
|
+
function createProjectAutoAssociationPlan() {
|
|
1581
|
+
return { fileMoves: [], jsonWrites: /* @__PURE__ */ new Map(), warnings: [] };
|
|
1582
|
+
}
|
|
1583
|
+
function planJsonWrite(autoAssociations, projectRoot, path, value) {
|
|
1584
|
+
if (!isPathInside(projectRoot, path))
|
|
1585
|
+
throw new UserError(`Cannot write inferred project metadata outside ${projectRoot}.`);
|
|
1586
|
+
autoAssociations.jsonWrites.set(relative(projectRoot, path).split(sep).join("/"), structuredClone(value));
|
|
1587
|
+
}
|
|
1588
|
+
async function directProjectFiles(directory) {
|
|
1589
|
+
try {
|
|
1590
|
+
return (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.isFile() && !entry.name.startsWith(".") && entry.name !== "file.json").map((entry) => entry.name).sort();
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
if (isFsError(error, "ENOENT")) return [];
|
|
1593
|
+
throw error;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function inferFileResourceId(fileName) {
|
|
1597
|
+
const extension = extname(fileName);
|
|
1598
|
+
const stem = extension ? fileName.slice(0, -extension.length) : fileName;
|
|
1599
|
+
const normalized = stem.normalize("NFKC").trim().replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
1600
|
+
return normalized || `file-${hash(fileName).slice(0, 8)}`;
|
|
1601
|
+
}
|
|
1602
|
+
function buildRelativePath(projectRelativePath) {
|
|
1603
|
+
return `../../${projectRelativePath}`;
|
|
1604
|
+
}
|
|
1605
|
+
async function readJsonObject(path) {
|
|
1606
|
+
let parsed;
|
|
1607
|
+
try {
|
|
1608
|
+
parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
throw new UserError(`${relative(process.cwd(), path)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (!isRecord2(parsed)) throw new UserError(`${path}: expected a JSON object.`);
|
|
1613
|
+
return parsed;
|
|
1614
|
+
}
|
|
1615
|
+
async function withProjectMutation(projectDirectory, kind, action) {
|
|
1616
|
+
const lease = await acquireDirectoryProjectMutation(projectDirectory, kind);
|
|
1617
|
+
try {
|
|
1618
|
+
return await action();
|
|
1619
|
+
} finally {
|
|
1620
|
+
await lease.release();
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
async function acquireDirectoryProjectMutation(projectDirectory, kind) {
|
|
1624
|
+
const projectRoot = resolve2(projectDirectory);
|
|
1625
|
+
const lockDirectory = resolve2(projectRoot, PROJECT_INTERNAL_DIRECTORY, "mutation.lock");
|
|
1626
|
+
await mkdir(dirname(lockDirectory), { recursive: true });
|
|
1627
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1628
|
+
try {
|
|
1629
|
+
await mkdir(lockDirectory);
|
|
1630
|
+
break;
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
if (!isFsError(error, "EEXIST")) throw error;
|
|
1633
|
+
if (attempt === 0 && await recoverDeadProjectMutation(lockDirectory)) continue;
|
|
1634
|
+
throw new DirectoryProjectMutationConflictError(
|
|
1635
|
+
"Project is busy with another Build, Publish, Restore, or Workbench write."
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
await writeFile2(
|
|
1640
|
+
resolve2(lockDirectory, "lease.json"),
|
|
1641
|
+
`${JSON.stringify({ pid: process.pid, kind, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
1642
|
+
`
|
|
1643
|
+
);
|
|
1644
|
+
let released = false;
|
|
1645
|
+
return {
|
|
1646
|
+
async release() {
|
|
1647
|
+
if (released) return;
|
|
1648
|
+
released = true;
|
|
1649
|
+
await rm2(lockDirectory, { recursive: true, force: true });
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
async function recoverDeadProjectMutation(lockDirectory) {
|
|
1654
|
+
let pid;
|
|
1655
|
+
try {
|
|
1656
|
+
const value = JSON.parse(await readFile2(resolve2(lockDirectory, "lease.json"), "utf8"));
|
|
1657
|
+
if (typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0) return false;
|
|
1658
|
+
pid = value.pid;
|
|
1659
|
+
} catch {
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
if (isProcessAlive(pid)) return false;
|
|
1663
|
+
const stale = `${lockDirectory}.stale.${randomUUID2()}`;
|
|
1664
|
+
try {
|
|
1665
|
+
await rename2(lockDirectory, stale);
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
return isFsError(error, "ENOENT");
|
|
1668
|
+
}
|
|
1669
|
+
await rm2(stale, { recursive: true, force: true });
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
function isProcessAlive(pid) {
|
|
1673
|
+
try {
|
|
1674
|
+
process.kill(pid, 0);
|
|
1675
|
+
return true;
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
return !isFsError(error, "ESRCH");
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
async function writeTextAtomic(path, source, mode = 420) {
|
|
1681
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1682
|
+
const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
1683
|
+
await writeFile2(temporary, source, { mode });
|
|
1684
|
+
await rename2(temporary, path);
|
|
1685
|
+
}
|
|
1686
|
+
function hashFiles(files) {
|
|
1687
|
+
const digest = createHash2("sha256");
|
|
1688
|
+
for (const file of files)
|
|
1689
|
+
digest.update(file.path).update("\0").update(String(file.mode)).update("\0").update(file.content).update("\0");
|
|
1690
|
+
return digest.digest("hex");
|
|
1691
|
+
}
|
|
1692
|
+
function hash(value) {
|
|
1693
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
1694
|
+
}
|
|
1695
|
+
function projectPublishPlanFingerprint(planned) {
|
|
1696
|
+
return hash(
|
|
1697
|
+
stableStringify({
|
|
1698
|
+
actions: planned.plan.actions,
|
|
1699
|
+
diagnostics: planned.plan.diagnostics
|
|
1700
|
+
})
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
function stableStringify(value) {
|
|
1704
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1705
|
+
if (value && typeof value === "object") {
|
|
1706
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
|
|
1707
|
+
}
|
|
1708
|
+
return JSON.stringify(value);
|
|
1709
|
+
}
|
|
1710
|
+
function sortObjectDeep(value) {
|
|
1711
|
+
if (Array.isArray(value)) return value.map(sortObjectDeep);
|
|
1712
|
+
if (!isRecord2(value)) return value;
|
|
1713
|
+
return Object.fromEntries(
|
|
1714
|
+
Object.keys(value).sort().map((key) => [key, sortObjectDeep(value[key])])
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
function hasErrors(diagnostics) {
|
|
1718
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
1719
|
+
}
|
|
1720
|
+
function toDiagnostic(error) {
|
|
1721
|
+
return {
|
|
1722
|
+
severity: "error",
|
|
1723
|
+
code: "project.directory.invalid",
|
|
1724
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function errorMessage(error) {
|
|
1728
|
+
return error instanceof Error ? error.message : String(error);
|
|
1729
|
+
}
|
|
1730
|
+
function assertSafeRelative(path) {
|
|
1731
|
+
if (!path || isAbsolute(path) || path.includes("\\") || path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
1732
|
+
throw new UserError(`Invalid project snapshot path: ${path}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function pathExists(path) {
|
|
1736
|
+
try {
|
|
1737
|
+
await stat(path);
|
|
1738
|
+
return true;
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
if (isFsError(error, "ENOENT")) return false;
|
|
1741
|
+
throw error;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function isRecord2(value) {
|
|
1745
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1746
|
+
}
|
|
1747
|
+
function isFsError(error, code) {
|
|
1748
|
+
return !!error && typeof error === "object" && "code" in error && error.code === code;
|
|
1749
|
+
}
|
|
1750
|
+
export {
|
|
1751
|
+
DIRECTORY_RESOURCE_TYPES,
|
|
1752
|
+
DirectoryProjectMutationConflictError,
|
|
1753
|
+
FILE_AUTO_ASSOCIATION_IGNORE_FILE,
|
|
1754
|
+
PROJECT_BUILD_FILE,
|
|
1755
|
+
PROJECT_BUILD_MANIFEST,
|
|
1756
|
+
PROJECT_INTERNAL_DIRECTORY,
|
|
1757
|
+
PROJECT_METADATA_FILE,
|
|
1758
|
+
PROJECT_STATE_FILE,
|
|
1759
|
+
acquireDirectoryProjectMutation,
|
|
1760
|
+
assertLegacyYamlNotShadowed,
|
|
1761
|
+
commitProjectBuild,
|
|
1762
|
+
createDirectoryWorkspaceVersionService,
|
|
1763
|
+
executeProjectPublish,
|
|
1764
|
+
getProjectBuildStatus,
|
|
1765
|
+
initializeDirectoryProject,
|
|
1766
|
+
inspectDirectoryProject,
|
|
1767
|
+
locateDirectoryProjectResource,
|
|
1768
|
+
planProjectPublish,
|
|
1769
|
+
previewProjectBuild,
|
|
1770
|
+
readValidProjectBuild,
|
|
1771
|
+
resolveDirectoryProjectRoot,
|
|
1772
|
+
resolveDirectoryProjectRuntime,
|
|
1773
|
+
validateDirectoryProject
|
|
1774
|
+
};
|