@openagentpack/sdk 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2429 @@
1
+ import {
2
+ ApiError,
3
+ ConflictError,
4
+ DeploymentCreateConflictError,
5
+ DiagnosticCollector,
6
+ UserError,
7
+ buildProviders,
8
+ collectProviderCapabilities,
9
+ collectReferenceDiagnostics,
10
+ extractSkillZipFiles,
11
+ getProvider,
12
+ isSupported,
13
+ resolveAgentMaterialization,
14
+ resolveFetch,
15
+ resolveTargetProviders,
16
+ skillNameFromFiles
17
+ } from "./chunk-ZBJDMUJT.js";
18
+
19
+ // src/internal/core/project-runtime.ts
20
+ function createProjectRuntime(input) {
21
+ const providers = buildProviders(input.providers ?? input.config.providers, input.projectName);
22
+ return {
23
+ configPath: input.configPath,
24
+ projectName: input.projectName,
25
+ config: input.config,
26
+ state: input.state,
27
+ providers
28
+ };
29
+ }
30
+ async function readProjectRuntime(input, fn) {
31
+ return input.stateBackend.read(
32
+ input.stateScope,
33
+ (state) => fn(
34
+ createProjectRuntime({
35
+ projectName: input.projectName,
36
+ config: input.config,
37
+ state,
38
+ configPath: input.configPath,
39
+ providers: input.providers
40
+ })
41
+ )
42
+ );
43
+ }
44
+ async function writeProjectRuntime(input, fn) {
45
+ return input.stateBackend.write(
46
+ input.stateScope,
47
+ (state) => fn(
48
+ createProjectRuntime({
49
+ projectName: input.projectName,
50
+ config: input.config,
51
+ state,
52
+ configPath: input.configPath,
53
+ providers: input.providers
54
+ })
55
+ )
56
+ );
57
+ }
58
+ function getRuntimeProvider(ctx, providerName) {
59
+ const adapter = ctx.providers.get(providerName);
60
+ if (!adapter) {
61
+ throw new UserError(`Provider '${providerName}' not configured.`);
62
+ }
63
+ return adapter;
64
+ }
65
+
66
+ // src/internal/core/skill-source.ts
67
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
68
+ import { basename, resolve as resolve2 } from "path";
69
+ import { parse } from "yaml";
70
+
71
+ // src/internal/utils/collect-files.ts
72
+ import { readdirSync, readFileSync, statSync } from "fs";
73
+ import { resolve } from "path";
74
+ function collectFiles(dir, base) {
75
+ const results = [];
76
+ for (const entry of readdirSync(dir).sort()) {
77
+ const fullPath = resolve(dir, entry);
78
+ const rel = base ? `${base}/${entry}` : entry;
79
+ const entryStat = statSync(fullPath);
80
+ if (entryStat.isFile()) {
81
+ results.push({ relativePath: rel, content: readFileSync(fullPath) });
82
+ } else if (entryStat.isDirectory()) {
83
+ results.push(...collectFiles(fullPath, rel));
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
89
+ // src/internal/core/skill-source.ts
90
+ async function inspectSkillSource(source, options = {}) {
91
+ if (/^https?:\/\//i.test(source)) {
92
+ throw new UserError("Skill source inspection only accepts a local directory, zip, or SKILL.md file.");
93
+ }
94
+ const sourcePath = resolve2(options.basePath ?? process.cwd(), source);
95
+ const sourceStat = statSync2(sourcePath, { throwIfNoEntry: false });
96
+ if (!sourceStat) throw new UserError(`Skill source not found: ${source}`);
97
+ let files;
98
+ if (sourceStat.isDirectory()) {
99
+ files = collectFiles(sourcePath, "");
100
+ } else if (sourceStat.isFile() && sourcePath.toLowerCase().endsWith(".zip")) {
101
+ files = await extractSkillZipFiles(readFileSync2(sourcePath));
102
+ } else if (sourceStat.isFile() && basename(sourcePath).toLowerCase() === "skill.md") {
103
+ files = [{ relativePath: "SKILL.md", content: readFileSync2(sourcePath) }];
104
+ } else {
105
+ throw new UserError("Skill source must be a directory, .zip archive, or SKILL.md file.");
106
+ }
107
+ const normalizedFiles = normalizeSkillRoot(files);
108
+ const manifest = normalizedFiles.find((file) => file.relativePath === "SKILL.md");
109
+ const name = parseSkillManifestName(manifest.content);
110
+ return { name, sourcePath, files: normalizedFiles };
111
+ }
112
+ function normalizeSkillRoot(files) {
113
+ if (files.some((file) => file.relativePath === "SKILL.md")) return files;
114
+ const manifests = files.filter((file) => file.relativePath.endsWith("/SKILL.md"));
115
+ if (manifests.length === 0) throw new UserError("Skill source does not contain SKILL.md.");
116
+ if (manifests.length > 1) {
117
+ throw new UserError("Skill source contains multiple SKILL.md files and has no unambiguous root.");
118
+ }
119
+ const prefix = manifests[0].relativePath.slice(0, -"SKILL.md".length);
120
+ return files.filter((file) => file.relativePath.startsWith(prefix)).map((file) => ({ ...file, relativePath: file.relativePath.slice(prefix.length) }));
121
+ }
122
+ function parseSkillManifestName(content) {
123
+ const text = content.toString("utf8");
124
+ const frontmatter = text.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\s*\r?\n|$)/);
125
+ if (!frontmatter) throw new UserError("SKILL.md must start with YAML frontmatter containing a name.");
126
+ let manifest;
127
+ try {
128
+ manifest = parse(frontmatter[1]);
129
+ } catch (error) {
130
+ throw new UserError(`Invalid SKILL.md frontmatter: ${error instanceof Error ? error.message : String(error)}`);
131
+ }
132
+ const rawName = manifest && typeof manifest === "object" ? manifest.name : void 0;
133
+ const name = typeof rawName === "string" ? rawName.trim() : "";
134
+ if (!name) throw new UserError("SKILL.md frontmatter must contain a non-empty name.");
135
+ return name;
136
+ }
137
+
138
+ // src/internal/executor/executor.ts
139
+ import { basename as basename2, dirname as dirname3, resolve as resolve4 } from "path";
140
+
141
+ // src/internal/utils/hash.ts
142
+ import { createHash } from "crypto";
143
+ function sha256(data) {
144
+ return createHash("sha256").update(data).digest("hex");
145
+ }
146
+ function contentHash(obj) {
147
+ const normalized = JSON.stringify(sortDeep(obj));
148
+ return sha256(normalized);
149
+ }
150
+ function sortDeep(obj) {
151
+ if (obj === null || obj === void 0) return obj;
152
+ if (Array.isArray(obj)) return obj.map(sortDeep);
153
+ if (typeof obj === "object") {
154
+ const sorted = {};
155
+ for (const key of Object.keys(obj).sort()) {
156
+ sorted[key] = sortDeep(obj[key]);
157
+ }
158
+ return sorted;
159
+ }
160
+ return obj;
161
+ }
162
+
163
+ // src/internal/planner/declaration.ts
164
+ function getResourceDeclaration(address, config) {
165
+ const { type, name } = address;
166
+ switch (type) {
167
+ case "environment":
168
+ return config.environments?.[name] ?? null;
169
+ case "vault":
170
+ return config.vaults?.[name] ?? null;
171
+ case "memory_store":
172
+ return config.memory_stores?.[name] ?? null;
173
+ case "skill":
174
+ return config.skills?.[name] ?? null;
175
+ case "agent":
176
+ case "template":
177
+ return config.agents?.[name] ?? null;
178
+ case "file":
179
+ return config.files?.[name] ?? null;
180
+ case "identity":
181
+ return config.identities?.[name] ?? null;
182
+ case "channel":
183
+ return config.channels?.[name] ?? null;
184
+ case "deployment":
185
+ return config.deployments?.[name] ?? null;
186
+ default:
187
+ return null;
188
+ }
189
+ }
190
+
191
+ // src/internal/planner/comparable.ts
192
+ function computeComparableDesiredHash(address, config, provider) {
193
+ const decl = getResourceDeclaration(address, config);
194
+ if (!decl || !provider.normalizeDesiredResource) return void 0;
195
+ const comparable = provider.normalizeDesiredResource(address.type, address.name, decl);
196
+ return comparable === null ? void 0 : contentHash(comparable);
197
+ }
198
+
199
+ // src/internal/planner/hasher.ts
200
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
201
+ import { dirname, resolve as resolve3 } from "path";
202
+ async function computeResourceHash(address, config, basePath, state) {
203
+ const decl = getDeclaration(address, config);
204
+ if (!decl) return "";
205
+ if (address.type === "skill") {
206
+ const skillDecl = decl;
207
+ const apiMode = resolveQoderApiMode(address.type, address.name, address.provider, config);
208
+ if (basePath) {
209
+ const fileHash = computeSkillContentHash(skillDecl.source, basePath);
210
+ return contentHash({ decl, fileHash, apiMode });
211
+ }
212
+ return contentHash({ decl, apiMode });
213
+ }
214
+ if (address.type === "environment" || address.type === "vault" || address.type === "memory_store") {
215
+ return contentHash({ decl, apiMode: resolveQoderApiMode(address.type, address.name, address.provider, config) });
216
+ }
217
+ if (address.type === "file" && basePath) {
218
+ const fileDecl = decl;
219
+ const fileHash = computeLocalFileContentHash(fileDecl.source, basePath);
220
+ return contentHash({
221
+ decl,
222
+ fileHash,
223
+ apiMode: resolveQoderApiMode("file", address.name, address.provider, config)
224
+ });
225
+ }
226
+ if (address.type === "deployment") {
227
+ const refs = resolveDeploymentReferenceIds(decl, config, address.provider, state);
228
+ const sourceHashes = basePath ? computeDeploymentSourceHashes(decl, basePath) : void 0;
229
+ if (refs || sourceHashes) return contentHash({ decl, refs, sourceHashes });
230
+ }
231
+ if (address.type === "template") {
232
+ const refs = resolveTemplateReferenceIds(decl, config, address.provider, state);
233
+ return contentHash({ decl: withoutLocalSessionFileMounts(decl), refs });
234
+ }
235
+ if (address.type === "agent") return contentHash(withoutLocalSessionFileMounts(decl));
236
+ if (address.type === "channel") {
237
+ const refs = resolveChannelReferenceIds(
238
+ decl,
239
+ config,
240
+ address.provider,
241
+ state
242
+ );
243
+ return contentHash({ decl, refs });
244
+ }
245
+ return contentHash(decl);
246
+ }
247
+ function withoutLocalSessionFileMounts(decl) {
248
+ if (!decl || typeof decl !== "object" || Array.isArray(decl)) return decl;
249
+ const { files, ...remoteDeclaration } = decl;
250
+ const templateFiles = Array.isArray(files) ? files.filter((file) => typeof file === "string") : [];
251
+ return templateFiles.length ? { ...remoteDeclaration, files: templateFiles } : remoteDeclaration;
252
+ }
253
+ function resolveQoderApiMode(type, name, provider, config) {
254
+ if (provider !== "qoder") return void 0;
255
+ if (type === "environment" && config.environments?.[name]?.environment_id) return "auto";
256
+ for (const agent of Object.values(config.agents ?? {})) {
257
+ if (agent.provider && agent.provider !== provider) continue;
258
+ const referenced = type === "environment" ? agent.environment === name : type === "skill" ? agent.skills?.some(
259
+ (skill) => typeof skill === "string" ? skill === name : skill.type === "custom" && skill.skill_id === name
260
+ ) : type === "vault" ? agent.vault === name : type === "memory_store" ? agent.memory_stores?.includes(name) : agent.files?.some((file) => (typeof file === "string" ? file : file.file) === name);
261
+ if (referenced && agent.delivery?.qoder?.type === "forward") return "forward";
262
+ }
263
+ return "managed";
264
+ }
265
+ function computeReplacementFingerprint(address, config) {
266
+ if (address.type !== "channel") return void 0;
267
+ const decl = config.channels?.[address.name];
268
+ if (!decl) return void 0;
269
+ return contentHash({ channel_type: decl.type, mode: decl.mode ?? "fixed", credentials: decl.credentials ?? {} });
270
+ }
271
+ function resolveChannelReferenceIds(decl, config, provider, state) {
272
+ if (decl.mode === "pairing" || !decl.agent) {
273
+ return { mode: "pairing" };
274
+ }
275
+ const agent = config.agents?.[decl.agent];
276
+ const agentType = agent?.delivery?.[provider]?.type === "forward" ? "template" : "agent";
277
+ const identity = decl.identity ?? config.defaults?.identity;
278
+ return {
279
+ agent_id: state?.getResource({ type: agentType, name: decl.agent, provider })?.remote_id,
280
+ identity_id: identity ? state?.getResource({ type: "identity", name: identity, provider })?.remote_id : void 0
281
+ };
282
+ }
283
+ function resolveTemplateReferenceIds(decl, config, provider, state) {
284
+ const environment = decl.environment ? config.environments?.[decl.environment] : void 0;
285
+ const tunnel = decl.tunnel ? config.tunnels?.[decl.tunnel] : void 0;
286
+ const skillIds = (decl.skills ?? []).map((skill) => {
287
+ if (typeof skill === "string") {
288
+ return state?.getResource({ type: "skill", name: skill, provider })?.remote_id ?? skill;
289
+ }
290
+ if (skill.type === "official") return `${skill.type}:${skill.skill_id}:${skill.version ?? ""}`;
291
+ return state?.getResource({ type: "skill", name: skill.skill_id, provider })?.remote_id ?? `${skill.type}:${skill.skill_id}:${skill.version ?? ""}`;
292
+ });
293
+ return {
294
+ environment_id: environment?.environment_id ?? (decl.environment ? state?.getResource({ type: "environment", name: decl.environment, provider })?.remote_id ?? void 0 : void 0),
295
+ tunnel_id: tunnel?.tunnel_id,
296
+ vault_ids: decl.vault ? [state?.getResource({ type: "vault", name: decl.vault, provider })?.remote_id ?? decl.vault] : [],
297
+ skill_ids: skillIds,
298
+ memory_store_ids: (decl.memory_stores ?? []).map(
299
+ (memoryStore) => state?.getResource({ type: "memory_store", name: memoryStore, provider })?.remote_id ?? memoryStore
300
+ ),
301
+ file_ids: (decl.files ?? []).filter((file) => typeof file === "string").map((file) => state?.getResource({ type: "file", name: file, provider })?.remote_id ?? file),
302
+ identity_id: decl.memory_stores?.length && config.defaults?.identity ? state?.getResource({ type: "identity", name: config.defaults.identity, provider })?.remote_id : void 0
303
+ };
304
+ }
305
+ function resolveDeploymentReferenceIds(decl, config, provider, state) {
306
+ const agent = config.agents?.[decl.agent];
307
+ const envName = decl.environment ?? agent?.environment;
308
+ if (!envName) return void 0;
309
+ const envDecl = config.environments?.[envName];
310
+ return {
311
+ environment_id: envDecl?.environment_id ?? (envDecl ? state?.getResource({ type: "environment", name: envName, provider })?.remote_id ?? void 0 : void 0)
312
+ };
313
+ }
314
+ function getDeclaration(address, config) {
315
+ return getResourceDeclaration(address, config);
316
+ }
317
+ function computeDeploymentSourceHashes(decl, basePath) {
318
+ const sources = [
319
+ ...new Set(
320
+ (decl.resources ?? []).flatMap(
321
+ (resource) => resource.type === "file" && !resource.file_id && resource.source ? [resource.source] : []
322
+ )
323
+ )
324
+ ];
325
+ if (sources.length === 0) return void 0;
326
+ return Object.fromEntries(sources.map((source) => [source, computeLocalFileContentHash(source, basePath)]));
327
+ }
328
+ function computeLocalFileContentHash(source, basePath) {
329
+ const fullPath = resolve3(dirname(basePath), source);
330
+ const stat = statSync3(fullPath, { throwIfNoEntry: false });
331
+ if (!stat?.isFile()) return "";
332
+ return contentHash(readFileSync3(fullPath).toString("base64"));
333
+ }
334
+ function computeSkillContentHash(source, basePath) {
335
+ const fullPath = resolve3(dirname(basePath), source);
336
+ const stat = statSync3(fullPath, { throwIfNoEntry: false });
337
+ if (stat?.isDirectory()) {
338
+ const parts = collectFiles(fullPath, "").map((file) => `${file.relativePath}:${file.content.toString("utf-8")}`);
339
+ return contentHash(parts.join("\n"));
340
+ }
341
+ if (stat?.isFile()) {
342
+ if (fullPath.endsWith(".zip")) {
343
+ const content2 = readFileSync3(fullPath);
344
+ return contentHash(content2.toString("base64"));
345
+ }
346
+ const content = readFileSync3(fullPath, "utf-8");
347
+ return contentHash(content);
348
+ }
349
+ return "";
350
+ }
351
+
352
+ // src/internal/planner/plan-semantics.ts
353
+ var NON_BLOCKING_ROOT_FIELDS = /* @__PURE__ */ new Set(["description", "metadata"]);
354
+ function diffChangedPaths(before, after, prefix = "") {
355
+ if (Object.is(before, after)) return [];
356
+ if (Array.isArray(before) || Array.isArray(after)) {
357
+ return structurallyEqual(before, after) ? [] : [prefix || "$root"];
358
+ }
359
+ if (isRecord(before) && isRecord(after)) {
360
+ const paths = [];
361
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
362
+ for (const key of [...keys].sort()) {
363
+ const path = prefix ? `${prefix}.${key}` : key;
364
+ paths.push(...diffChangedPaths(before[key], after[key], path));
365
+ }
366
+ return paths;
367
+ }
368
+ return [prefix || "$root"];
369
+ }
370
+ function classifyReadinessImpact(action, changedPaths) {
371
+ if (action === "no-op") return "none";
372
+ if (action !== "update" || !changedPaths || changedPaths.length === 0) return "blocking";
373
+ return changedPaths.every(isNonBlockingPath) ? "non_blocking" : "blocking";
374
+ }
375
+ function buildReadinessBaseline(declaration) {
376
+ const record = isRecord(declaration) ? declaration : {};
377
+ const { description: _description, metadata: _metadata, ...operational } = record;
378
+ return {
379
+ operational_hash: contentHash(operational),
380
+ description_hash: contentHash(record.description ?? null),
381
+ metadata_hash: contentHash(record.metadata ?? null)
382
+ };
383
+ }
384
+ function diffReadinessBaseline(before, after) {
385
+ const paths = [];
386
+ if (before.operational_hash !== after.operational_hash) paths.push("$operational");
387
+ if (before.description_hash !== after.description_hash) paths.push("description");
388
+ if (before.metadata_hash !== after.metadata_hash) paths.push("metadata");
389
+ return paths;
390
+ }
391
+ function isNonBlockingPath(path) {
392
+ const root = path.split(".", 1)[0];
393
+ return root !== void 0 && NON_BLOCKING_ROOT_FIELDS.has(root);
394
+ }
395
+ function isRecord(value) {
396
+ return typeof value === "object" && value !== null && !Array.isArray(value);
397
+ }
398
+ function structurallyEqual(left, right) {
399
+ return contentHash(left) === contentHash(right);
400
+ }
401
+
402
+ // src/internal/providers/drift-support.ts
403
+ function supportsFullDrift(adapter, type) {
404
+ return adapter.getDriftSupport?.(type) === "full" && typeof adapter.readComparableResource === "function";
405
+ }
406
+ async function readComparableIfSupported(adapter, type, id, name) {
407
+ if (!supportsFullDrift(adapter, type)) return null;
408
+ if (typeof adapter.readComparableResource !== "function") return null;
409
+ try {
410
+ return await adapter.readComparableResource(type, id, name);
411
+ } catch {
412
+ return null;
413
+ }
414
+ }
415
+
416
+ // src/internal/types/runtime-feedback.ts
417
+ function emitRuntimeFeedback(sink, event) {
418
+ sink?.(event);
419
+ }
420
+
421
+ // src/internal/types/state.ts
422
+ function addressKey(addr) {
423
+ return `${addr.provider}.${addr.type}.${addr.name}`;
424
+ }
425
+
426
+ // src/internal/executor/resolver.ts
427
+ function resolveRef(state, address) {
428
+ return state.getResource(address)?.remote_id;
429
+ }
430
+ function requireRef(state, address) {
431
+ const id = resolveRef(state, address);
432
+ if (!id) {
433
+ throw new UserError(
434
+ `Resource ${address.provider}.${address.type}.${address.name} not found in state. Run \`agents apply\` first.`
435
+ );
436
+ }
437
+ return id;
438
+ }
439
+ function resolveAgentRefs(agentName, config, provider, state) {
440
+ const agent = config.agents?.[agentName];
441
+ if (!agent) throw new UserError(`Agent '${agentName}' not found in config`);
442
+ const refs = {
443
+ skill_ids: []
444
+ };
445
+ if (agent.skills) {
446
+ for (const skill of agent.skills) {
447
+ if (typeof skill === "string") {
448
+ const id = requireRef(state, { type: "skill", name: skill, provider });
449
+ refs.skill_ids.push({ type: "custom", skill_id: id });
450
+ } else {
451
+ const resolvedId = skill.type === "custom" ? resolveRef(state, {
452
+ type: "skill",
453
+ name: skill.skill_id,
454
+ provider
455
+ }) ?? skill.skill_id : skill.skill_id;
456
+ refs.skill_ids.push({
457
+ type: skill.type,
458
+ skill_id: resolvedId,
459
+ version: skill.version
460
+ });
461
+ }
462
+ }
463
+ }
464
+ if (agent.multiagent) {
465
+ refs.multiagent_agent_ids = [];
466
+ for (const subName of agent.multiagent.agents) {
467
+ const id = resolveRef(state, { type: "agent", name: subName, provider });
468
+ if (id) refs.multiagent_agent_ids.push(id);
469
+ }
470
+ }
471
+ return refs;
472
+ }
473
+ function resolveTemplateRefs(agentName, config, provider, state) {
474
+ const agent = config.agents?.[agentName];
475
+ if (!agent) throw new UserError(`Agent '${agentName}' not found in config`);
476
+ if (!agent.environment) {
477
+ throw new UserError(`Forward template '${agentName}' must declare an environment.`);
478
+ }
479
+ const environment = config.environments?.[agent.environment];
480
+ if (!environment) throw new UserError(`Environment '${agent.environment}' is not defined in config.`);
481
+ const agentRefs = resolveAgentRefs(agentName, config, provider, state);
482
+ const memoryStoreIds = (agent.memory_stores ?? []).map(
483
+ (memoryStore) => requireRef(state, { type: "memory_store", name: memoryStore, provider })
484
+ );
485
+ const identityName = config.defaults?.identity;
486
+ return {
487
+ ...agentRefs,
488
+ environment_id: environment.environment_id ?? requireRef(state, { type: "environment", name: agent.environment, provider }),
489
+ ...agent.tunnel ? { tunnel_id: resolveTunnelIdFromConfig(config, agent.tunnel, provider) } : {},
490
+ vault_ids: agent.vault ? [requireRef(state, { type: "vault", name: agent.vault, provider })] : [],
491
+ file_ids: (agent.files ?? []).filter((file) => typeof file === "string").map((file) => requireRef(state, { type: "file", name: file, provider })),
492
+ memory_store_ids: memoryStoreIds,
493
+ owned_memory_store_ids: state.listResources().filter(
494
+ (resource) => resource.address.provider === provider && resource.address.type === "memory_store" && resource.api_mode === "forward" && typeof resource.remote_id === "string"
495
+ ).map((resource) => resource.remote_id),
496
+ ...identityName ? { identity_id: requireRef(state, { type: "identity", name: identityName, provider }) } : {}
497
+ };
498
+ }
499
+ function resolveDeploymentRefs(deploymentName, config, provider, state) {
500
+ const dep = config.deployments?.[deploymentName];
501
+ if (!dep) throw new UserError(`Deployment '${deploymentName}' not found in config`);
502
+ const agent = config.agents?.[dep.agent];
503
+ if (!agent) {
504
+ throw new UserError(`Deployment '${deploymentName}' references unknown agent '${dep.agent}'`);
505
+ }
506
+ const agent_id = requireRef(state, {
507
+ type: "agent",
508
+ name: dep.agent,
509
+ provider
510
+ });
511
+ const envName = dep.environment ?? agent.environment;
512
+ if (!envName) {
513
+ throw new UserError(
514
+ `Deployment '${deploymentName}' has no environment and agent '${dep.agent}' does not declare one`
515
+ );
516
+ }
517
+ const envDecl = config.environments?.[envName];
518
+ if (!envDecl) {
519
+ throw new UserError(`Environment '${envName}' is not defined in config.`);
520
+ }
521
+ const environment_id = envDecl.environment_id ?? requireRef(state, {
522
+ type: "environment",
523
+ name: envName,
524
+ provider
525
+ });
526
+ const tunnelName = dep.tunnel ?? agent.tunnel;
527
+ const tunnel_id = tunnelName ? resolveTunnelIdFromConfig(config, tunnelName, provider) : void 0;
528
+ const vaultNames = dep.vaults ?? (agent.vault ? [agent.vault] : []);
529
+ const vault_ids = vaultNames.map((v) => requireRef(state, { type: "vault", name: v, provider }));
530
+ const msNames = /* @__PURE__ */ new Set();
531
+ for (const m of dep.memory_stores ?? []) msNames.add(m);
532
+ for (const r of dep.resources ?? []) {
533
+ if (r.type === "memory_store") msNames.add(r.memory_store);
534
+ }
535
+ if (msNames.size === 0) {
536
+ for (const m of agent.memory_stores ?? []) msNames.add(m);
537
+ }
538
+ const memory_store_ids = {};
539
+ for (const m of msNames) {
540
+ memory_store_ids[m] = requireRef(state, {
541
+ type: "memory_store",
542
+ name: m,
543
+ provider
544
+ });
545
+ }
546
+ return {
547
+ agent_id,
548
+ agent_version: dep.agent_version,
549
+ environment_id,
550
+ tunnel_id,
551
+ vault_ids,
552
+ memory_store_ids
553
+ };
554
+ }
555
+ function resolveChannelRefs(channelName, config, provider, state) {
556
+ const channel = config.channels?.[channelName];
557
+ if (!channel) throw new UserError(`Channel '${channelName}' not found in config`);
558
+ if (channel.mode === "pairing") {
559
+ return {};
560
+ }
561
+ if (!channel.agent) {
562
+ throw new UserError(`Channel '${channelName}' is fixed mode and must declare agent`);
563
+ }
564
+ const agent = config.agents?.[channel.agent];
565
+ if (!agent) throw new UserError(`Channel '${channelName}' references unknown agent '${channel.agent}'`);
566
+ const agentType = agent.delivery?.[provider]?.type === "forward" ? "template" : "agent";
567
+ const agent_id = requireRef(state, { type: agentType, name: channel.agent, provider });
568
+ const identityName = channel.identity ?? config.defaults?.identity;
569
+ if (!identityName) {
570
+ throw new UserError(`Channel '${channelName}' must declare identity or use defaults.identity.`);
571
+ }
572
+ const identity_id = requireRef(state, { type: "identity", name: identityName, provider });
573
+ return { identity_id, agent_id };
574
+ }
575
+ function resolveTunnelIdFromConfig(config, tunnelName, provider) {
576
+ if (provider !== "qoder") {
577
+ throw new UserError("Tunnels are supported only by Qoder BYOC sessions.");
578
+ }
579
+ const tunnel = config.tunnels?.[tunnelName];
580
+ if (!tunnel) {
581
+ throw new UserError(`Tunnel '${tunnelName}' is not defined in config. Declare it under the 'tunnels:' section.`);
582
+ }
583
+ return tunnel.tunnel_id;
584
+ }
585
+
586
+ // src/internal/executor/skill-resolver.ts
587
+ import { dirname as dirname2 } from "path";
588
+ async function resolveSkillFiles(decl, ctx) {
589
+ if (/^https?:\/\//i.test(decl.source)) {
590
+ const res = await resolveFetch()(decl.source);
591
+ if (!res.ok) throw new Error(`skill source \u4E0B\u8F7D\u5931\u8D25\uFF1A${res.status} ${decl.source}`);
592
+ return extractSkillZipFiles(Buffer.from(await res.arrayBuffer()));
593
+ }
594
+ if (!ctx.configPath) return [];
595
+ return (await inspectSkillSource(decl.source, { basePath: dirname2(ctx.configPath) })).files;
596
+ }
597
+
598
+ // src/internal/executor/executor.ts
599
+ function memoryStoreUnsupported(provider) {
600
+ return new UserError(`Provider '${provider}' does not support memory stores`);
601
+ }
602
+ var DEFAULT_CONCURRENCY = 6;
603
+ var MAX_CONCURRENCY = 10;
604
+ function clampConcurrency(value) {
605
+ if (value === void 0 || !Number.isFinite(value)) return DEFAULT_CONCURRENCY;
606
+ return Math.max(1, Math.min(MAX_CONCURRENCY, Math.floor(value)));
607
+ }
608
+ async function executePlan(plan, ctx, options = {}) {
609
+ const concurrency = clampConcurrency(options.concurrency);
610
+ const resultsByKey = /* @__PURE__ */ new Map();
611
+ const failed = /* @__PURE__ */ new Set();
612
+ let stateUpdated = false;
613
+ for (const action of plan.actions) {
614
+ if (action.address.type !== "environment" && action.address.type !== "identity") continue;
615
+ const existing = ctx.state.getResource(action.address);
616
+ const externalId = action.address.type === "environment" ? ctx.config.environments?.[action.address.name]?.environment_id : ctx.config.identities?.[action.address.name]?.identity_id;
617
+ if (externalId && existing && !existing.externally_managed) {
618
+ ctx.state.setResource({ ...existing, externally_managed: true });
619
+ stateUpdated = true;
620
+ }
621
+ }
622
+ if (stateUpdated) await ctx.state.save();
623
+ const actionable = plan.actions.filter((a) => a.action !== "no-op");
624
+ const mutations = actionable.filter((a) => a.action !== "delete");
625
+ const deletions = actionable.filter((a) => a.action === "delete");
626
+ const runAction = async (action) => {
627
+ const key = addressKey(action.address);
628
+ const depFailed = action.dependencies.some((d) => failed.has(addressKey(d)));
629
+ if (depFailed) {
630
+ resultsByKey.set(key, { action, status: "skipped" });
631
+ failed.add(key);
632
+ return;
633
+ }
634
+ const provider = ctx.providers.get(action.address.provider);
635
+ if (!provider) {
636
+ resultsByKey.set(key, {
637
+ action,
638
+ status: "failed",
639
+ error: new Error(`Provider '${action.address.provider}' not configured`)
640
+ });
641
+ failed.add(key);
642
+ return;
643
+ }
644
+ try {
645
+ const adopted = await executeAction(action, provider, ctx);
646
+ resultsByKey.set(key, { action, status: "success" });
647
+ if (!adopted) {
648
+ emitRuntimeFeedback(ctx.onFeedback, {
649
+ type: "resource_action_success",
650
+ level: "success",
651
+ action,
652
+ resource: action.address,
653
+ message: `${action.action} ${action.address.type}.${action.address.name} (${action.address.provider})`
654
+ });
655
+ }
656
+ } catch (err) {
657
+ const error = err instanceof Error ? err : new Error(String(err));
658
+ resultsByKey.set(key, { action, status: "failed", error });
659
+ failed.add(key);
660
+ emitRuntimeFeedback(ctx.onFeedback, {
661
+ type: "resource_action_failed",
662
+ level: "error",
663
+ action,
664
+ resource: action.address,
665
+ message: `Failed to ${action.action} ${action.address.type}.${action.address.name}: ${error.message}`
666
+ });
667
+ }
668
+ };
669
+ for (const level of buildActionLevels(mutations)) {
670
+ await runWithConcurrency(level, concurrency, runAction);
671
+ await ctx.state.save();
672
+ }
673
+ if (!ctx.createOnly) {
674
+ await reconcileDefaultMemoryStores(ctx, new Set(plan.actions.map((action) => action.address.provider)));
675
+ }
676
+ for (const action of deletions) {
677
+ await runAction(action);
678
+ await ctx.state.save();
679
+ }
680
+ const results = actionable.map((a) => resultsByKey.get(addressKey(a.address)));
681
+ return {
682
+ results,
683
+ partial: results.some((r) => r.status === "failed")
684
+ };
685
+ }
686
+ async function reconcileDefaultMemoryStores(ctx, plannedProviders) {
687
+ const identityName = ctx.config.defaults?.identity;
688
+ for (const [agentName, agent] of Object.entries(ctx.config.agents ?? {})) {
689
+ const desired = agent.default_memory_store;
690
+ if (!desired || agent.delivery?.qoder?.type !== "forward") continue;
691
+ const providerName = "qoder";
692
+ if (agent.provider && agent.provider !== providerName || !plannedProviders.has(providerName)) continue;
693
+ const provider = ctx.providers.get(providerName);
694
+ if (!provider?.reconcileDefaultMemoryStore || !identityName) continue;
695
+ const identityId = ctx.state.getResource({
696
+ type: "identity",
697
+ name: identityName,
698
+ provider: providerName
699
+ })?.remote_id;
700
+ const templateId = ctx.state.getResource({ type: "template", name: agentName, provider: providerName })?.remote_id;
701
+ if (!identityId || !templateId) continue;
702
+ const result = await provider.reconcileDefaultMemoryStore(identityId, templateId, desired);
703
+ const resource = { type: "template", name: agentName, provider: providerName };
704
+ if (result.status === "pending") {
705
+ emitRuntimeFeedback(ctx.onFeedback, {
706
+ type: "provider_wait",
707
+ level: "warning",
708
+ resource,
709
+ message: `default memory store for template.${agentName} is pending \u2014 create the first Forward Session, then run apply again`
710
+ });
711
+ } else if (result.status === "updated") {
712
+ emitRuntimeFeedback(ctx.onFeedback, {
713
+ type: "resource_action_success",
714
+ level: "success",
715
+ resource,
716
+ message: `updated default memory store for template.${agentName} to "${desired.name}"`
717
+ });
718
+ }
719
+ }
720
+ }
721
+ async function runWithConcurrency(items, limit, worker) {
722
+ if (items.length === 0) return;
723
+ let cursor = 0;
724
+ const size = Math.min(limit, items.length);
725
+ const runners = [];
726
+ for (let i = 0; i < size; i++) {
727
+ runners.push(
728
+ (async () => {
729
+ while (cursor < items.length) {
730
+ const item = items[cursor++];
731
+ await worker(item);
732
+ }
733
+ })()
734
+ );
735
+ }
736
+ await Promise.all(runners);
737
+ }
738
+ function buildActionLevels(actions) {
739
+ const inSet = new Set(actions.map((a) => addressKey(a.address)));
740
+ const remaining = /* @__PURE__ */ new Map();
741
+ const deps = /* @__PURE__ */ new Map();
742
+ for (const action of actions) {
743
+ const key = addressKey(action.address);
744
+ remaining.set(key, action);
745
+ const scoped = /* @__PURE__ */ new Set();
746
+ for (const dep of action.dependencies) {
747
+ const depKey = addressKey(dep);
748
+ if (inSet.has(depKey)) scoped.add(depKey);
749
+ }
750
+ deps.set(key, scoped);
751
+ }
752
+ const levels = [];
753
+ while (remaining.size > 0) {
754
+ const level = [];
755
+ for (const [key, action] of remaining) {
756
+ const scoped = deps.get(key);
757
+ let ready = true;
758
+ for (const depKey of scoped) {
759
+ if (remaining.has(depKey)) {
760
+ ready = false;
761
+ break;
762
+ }
763
+ }
764
+ if (ready) level.push(action);
765
+ }
766
+ if (level.length === 0) {
767
+ levels.push([...remaining.values()]);
768
+ break;
769
+ }
770
+ for (const action of level) remaining.delete(addressKey(action.address));
771
+ levels.push(level);
772
+ }
773
+ return levels;
774
+ }
775
+ async function executeAction(action, provider, ctx) {
776
+ try {
777
+ return await executeActionInner(action, provider, ctx);
778
+ } catch (err) {
779
+ if (action.action !== "update" || !ApiError.isNotFound(err)) throw err;
780
+ emitRuntimeFeedback(ctx.onFeedback, {
781
+ type: "resource_already_gone",
782
+ level: "warning",
783
+ action,
784
+ resource: action.address,
785
+ message: `update ${action.address.type}.${action.address.name} (${action.address.provider}) \u2014 not found remotely, recreating`
786
+ });
787
+ ctx.state.removeResource(action.previousAddress ?? action.address);
788
+ return executeActionInner({ ...action, action: "create", previousAddress: void 0 }, provider, ctx);
789
+ }
790
+ }
791
+ async function executeActionInner(action, provider, ctx) {
792
+ const { address } = action;
793
+ const { type, name } = address;
794
+ let adopted = false;
795
+ if (action.action === "delete") {
796
+ const existing = ctx.state.getResource(address);
797
+ if (!existing) return false;
798
+ const id = existing.remote_id;
799
+ const apiMode2 = existing.api_mode;
800
+ if (type === "environment" || type === "identity") {
801
+ const externalReference = type === "environment" ? ctx.config.environments?.[name]?.environment_id : ctx.config.identities?.[name]?.identity_id;
802
+ if (existing.externally_managed || externalReference) {
803
+ ctx.state.removeResource(address);
804
+ return false;
805
+ }
806
+ }
807
+ if (id !== null) {
808
+ try {
809
+ switch (type) {
810
+ case "environment":
811
+ await provider.deleteEnvironment(id, false, apiMode2);
812
+ break;
813
+ case "vault":
814
+ await provider.deleteVault(id, apiMode2);
815
+ break;
816
+ case "skill":
817
+ await provider.deleteSkill(id, apiMode2);
818
+ break;
819
+ case "agent":
820
+ await provider.deleteAgent(id);
821
+ break;
822
+ case "template":
823
+ if (!provider.archiveTemplate)
824
+ throw new UserError(`Provider '${address.provider}' does not support templates`);
825
+ await provider.archiveTemplate(id, ownedForwardMemoryStoreIds(ctx, address.provider));
826
+ break;
827
+ case "memory_store":
828
+ if (!provider.deleteMemoryStore) throw memoryStoreUnsupported(address.provider);
829
+ await provider.deleteMemoryStore(id, apiMode2);
830
+ break;
831
+ case "deployment":
832
+ await provider.deleteDeployment(id);
833
+ break;
834
+ case "file":
835
+ await provider.deleteFile(id, apiMode2);
836
+ break;
837
+ case "identity":
838
+ if (!provider.deleteIdentity)
839
+ throw new UserError(`Provider '${address.provider}' does not support identities`);
840
+ await provider.deleteIdentity(id);
841
+ break;
842
+ case "channel":
843
+ if (!provider.deleteChannel)
844
+ throw new UserError(`Provider '${address.provider}' does not support channels`);
845
+ await provider.deleteChannel(id);
846
+ break;
847
+ }
848
+ } catch (err) {
849
+ if (!ApiError.isNotFound(err)) throw err;
850
+ emitRuntimeFeedback(ctx.onFeedback, {
851
+ type: "resource_already_gone",
852
+ level: "warning",
853
+ resource: address,
854
+ message: `${type}.${name} (${address.provider}) \u2014 already deleted remotely, cleaning up state`
855
+ });
856
+ }
857
+ }
858
+ ctx.state.removeResource(address);
859
+ return false;
860
+ }
861
+ const isUpdate = action.action === "update";
862
+ const priorAddress = action.previousAddress ?? address;
863
+ const existingId = isUpdate ? ctx.state.getResource(priorAddress)?.remote_id : void 0;
864
+ const apiMode = resolveResourceApiMode(type, name, address.provider, ctx.config);
865
+ const priorApiMode = ctx.state.getResource(priorAddress)?.api_mode ?? (address.provider === "qoder" ? "managed" : void 0);
866
+ const apiModeChanged = isUpdate && apiMode !== void 0 && priorApiMode !== apiMode;
867
+ let result;
868
+ switch (type) {
869
+ case "environment": {
870
+ const decl = ctx.config.environments[name];
871
+ const remoteName = decl.name ?? name;
872
+ if (decl.environment_id) {
873
+ result = { id: decl.environment_id, type: "environment" };
874
+ emitRuntimeFeedback(ctx.onFeedback, {
875
+ type: "resource_action_success",
876
+ level: "info",
877
+ action,
878
+ resource: action.address,
879
+ message: `${action.action} ${action.address.type}.${action.address.name} (${action.address.provider}) \u2014 external reference, no remote mutation`
880
+ });
881
+ } else if (apiModeChanged) {
882
+ try {
883
+ result = await provider.createEnvironment(remoteName, decl, apiMode);
884
+ } catch (err) {
885
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
886
+ mode: apiMode,
887
+ createOnly: ctx.createOnly,
888
+ onExisting: (existing) => provider.updateEnvironment(existing.id, remoteName, decl, apiMode)
889
+ });
890
+ adopted = true;
891
+ }
892
+ if (existingId) await provider.deleteEnvironment(existingId, false, priorApiMode);
893
+ } else if (isUpdate) {
894
+ const prior = ctx.state.getResource(address);
895
+ if (prior?.externally_managed) {
896
+ throw new UserError(
897
+ `environment.${name} is recorded as an external reference (${prior.remote_id ?? "unknown id"}); refusing to modify it remotely. Restore 'environment_id' to keep it as a reference, or release it first with 'agents state rm environment.${name}' (then 'agents state import' to adopt it as a managed resource).`
898
+ );
899
+ }
900
+ result = await provider.updateEnvironment(existingId, remoteName, decl, apiMode);
901
+ } else {
902
+ try {
903
+ result = await provider.createEnvironment(remoteName, decl, apiMode);
904
+ } catch (err) {
905
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
906
+ mode: apiMode,
907
+ createOnly: ctx.createOnly,
908
+ onExisting: (existing) => provider.updateEnvironment(existing.id, remoteName, decl, apiMode)
909
+ });
910
+ adopted = true;
911
+ }
912
+ }
913
+ break;
914
+ }
915
+ case "vault": {
916
+ const decl = ctx.config.vaults[name];
917
+ if (apiModeChanged) {
918
+ result = await provider.createVault(name, decl, apiMode);
919
+ if (existingId) await provider.deleteVault(existingId, priorApiMode).catch(() => void 0);
920
+ } else if (isUpdate) {
921
+ try {
922
+ result = await provider.createVault(name, decl, apiMode);
923
+ await provider.deleteVault(existingId, apiMode);
924
+ } catch {
925
+ await provider.deleteVault(existingId, apiMode);
926
+ result = await provider.createVault(name, decl, apiMode);
927
+ }
928
+ } else {
929
+ try {
930
+ result = await provider.createVault(name, decl, apiMode);
931
+ } catch (err) {
932
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
933
+ mode: apiMode,
934
+ createOnly: ctx.createOnly,
935
+ onExisting: async (existing) => {
936
+ await provider.deleteVault(existing.id, apiMode);
937
+ return provider.createVault(name, decl, apiMode);
938
+ }
939
+ });
940
+ adopted = true;
941
+ }
942
+ }
943
+ break;
944
+ }
945
+ case "skill": {
946
+ const decl = ctx.config.skills[name];
947
+ if (decl.origin && decl.origin !== "custom") {
948
+ emitRuntimeFeedback(ctx.onFeedback, {
949
+ type: "resource_action_success",
950
+ level: "info",
951
+ action,
952
+ resource: address,
953
+ message: `skip skill.${name} (${address.provider}) \u2014 official skill, no upload needed`
954
+ });
955
+ result = { id: null, type: "skill" };
956
+ adopted = true;
957
+ break;
958
+ }
959
+ const remoteName = decl.name ?? name;
960
+ const files = await resolveSkillFiles(decl, ctx);
961
+ if (apiModeChanged) {
962
+ result = await provider.createSkill(remoteName, decl, files, apiMode);
963
+ if (existingId) await provider.deleteSkill(existingId, priorApiMode).catch(() => void 0);
964
+ } else if (isUpdate) {
965
+ result = await provider.updateSkill(existingId, remoteName, decl, files, apiMode);
966
+ } else {
967
+ const manifestName = skillNameFromFiles(files);
968
+ const searchNames = manifestName && manifestName !== remoteName ? [remoteName, manifestName] : [remoteName];
969
+ const existing = await findExistingByNames(provider, "skill", searchNames, apiMode);
970
+ if (existing) {
971
+ if (ctx.createOnly) throw createOnlyExistingResourceError(address, existing.name);
972
+ result = existing.resource;
973
+ emitRuntimeFeedback(ctx.onFeedback, {
974
+ type: "resource_adopted",
975
+ level: "info",
976
+ resource: address,
977
+ message: `adopt skill.${name} (${address.provider}) \u2014 already existed remotely as "${existing.name}"`
978
+ });
979
+ adopted = true;
980
+ } else {
981
+ try {
982
+ result = await provider.createSkill(remoteName, decl, files, apiMode);
983
+ } catch (err) {
984
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
985
+ mode: apiMode,
986
+ searchNames,
987
+ createOnly: ctx.createOnly,
988
+ onExisting: async (existing2) => existing2
989
+ });
990
+ adopted = true;
991
+ }
992
+ }
993
+ }
994
+ break;
995
+ }
996
+ case "memory_store": {
997
+ const createMemoryStore = provider.createMemoryStore?.bind(provider);
998
+ const deleteMemoryStore = provider.deleteMemoryStore?.bind(provider);
999
+ if (!createMemoryStore || !deleteMemoryStore) throw memoryStoreUnsupported(address.provider);
1000
+ const decl = ctx.config.memory_stores[name];
1001
+ if (!provider.updateMemoryStore || !provider.listMemories || !provider.createMemory || !provider.updateMemory) {
1002
+ throw memoryStoreUnsupported(address.provider);
1003
+ }
1004
+ const reconcile = async (storeId) => {
1005
+ const store = await provider.updateMemoryStore(
1006
+ storeId,
1007
+ {
1008
+ name,
1009
+ description: decl.description,
1010
+ metadata: decl.metadata ?? {}
1011
+ },
1012
+ apiMode
1013
+ );
1014
+ const current = /* @__PURE__ */ new Map();
1015
+ let cursor;
1016
+ do {
1017
+ const page = await provider.listMemories(storeId, { limit: 100, cursor, view: "basic" }, apiMode);
1018
+ for (const memory of page.data) {
1019
+ if (memory.type === "memory") current.set(memory.path, memory);
1020
+ }
1021
+ cursor = page.has_more ? page.next_cursor : void 0;
1022
+ } while (cursor);
1023
+ for (const entry of decl.entries ?? []) {
1024
+ const existing = current.get(entry.key.replace(/^\/+/, ""));
1025
+ if (existing) {
1026
+ if (existing.content_sha256 !== sha256(entry.content)) {
1027
+ await provider.updateMemory(
1028
+ storeId,
1029
+ existing.id,
1030
+ {
1031
+ content: entry.content,
1032
+ expected_content_sha256: existing.content_sha256
1033
+ },
1034
+ apiMode
1035
+ );
1036
+ }
1037
+ } else {
1038
+ await provider.createMemory(storeId, { path: entry.key, content: entry.content }, apiMode);
1039
+ }
1040
+ }
1041
+ return store;
1042
+ };
1043
+ if (apiModeChanged) {
1044
+ result = await createMemoryStore(name, decl, apiMode);
1045
+ if (existingId) await deleteMemoryStore(existingId, priorApiMode).catch(() => void 0);
1046
+ } else if (isUpdate) {
1047
+ result = await reconcile(existingId);
1048
+ } else {
1049
+ try {
1050
+ result = await createMemoryStore(name, decl, apiMode);
1051
+ } catch (err) {
1052
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
1053
+ mode: apiMode,
1054
+ createOnly: ctx.createOnly,
1055
+ onExisting: async (existing) => reconcile(existing.id)
1056
+ });
1057
+ adopted = true;
1058
+ }
1059
+ }
1060
+ break;
1061
+ }
1062
+ case "agent": {
1063
+ const decl = ctx.config.agents[name];
1064
+ const remoteName = decl.name ?? name;
1065
+ const refs = resolveAgentRefs(name, ctx.config, address.provider, ctx.state);
1066
+ if (isUpdate) {
1067
+ result = await provider.updateAgent(existingId, remoteName, decl, refs);
1068
+ } else {
1069
+ try {
1070
+ result = await provider.createAgent(remoteName, decl, refs);
1071
+ } catch (err) {
1072
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
1073
+ createOnly: ctx.createOnly,
1074
+ onExisting: (existing) => provider.updateAgent(existing.id, remoteName, decl, refs)
1075
+ });
1076
+ adopted = true;
1077
+ }
1078
+ }
1079
+ break;
1080
+ }
1081
+ case "template": {
1082
+ const createTemplate = provider.createTemplate?.bind(provider);
1083
+ const updateTemplate = provider.updateTemplate?.bind(provider);
1084
+ if (!createTemplate || !updateTemplate) {
1085
+ throw new UserError(`Provider '${address.provider}' does not support templates`);
1086
+ }
1087
+ const decl = ctx.config.agents[name];
1088
+ const remoteName = decl.name ?? name;
1089
+ const refs = resolveTemplateRefs(name, ctx.config, address.provider, ctx.state);
1090
+ if (isUpdate) {
1091
+ result = await updateTemplate(existingId, remoteName, decl, refs);
1092
+ } else {
1093
+ try {
1094
+ result = await createTemplate(remoteName, decl, refs);
1095
+ } catch (err) {
1096
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
1097
+ createOnly: ctx.createOnly,
1098
+ onExisting: (existing) => updateTemplate(existing.id, remoteName, decl, refs)
1099
+ });
1100
+ adopted = true;
1101
+ }
1102
+ }
1103
+ break;
1104
+ }
1105
+ case "identity": {
1106
+ const createIdentity = provider.createIdentity?.bind(provider);
1107
+ const updateIdentity = provider.updateIdentity?.bind(provider);
1108
+ if (!createIdentity || !updateIdentity) {
1109
+ throw new UserError(`Provider '${address.provider}' does not support identities`);
1110
+ }
1111
+ const decl = ctx.config.identities[name];
1112
+ if (decl.identity_id) {
1113
+ const remote2 = await provider.findResource("identity", name, decl.identity_id);
1114
+ if (!remote2?.id) {
1115
+ throw new UserError(
1116
+ `External identity.${name} '${decl.identity_id}' was not found on provider '${address.provider}'.`
1117
+ );
1118
+ }
1119
+ result = remote2;
1120
+ break;
1121
+ }
1122
+ if (isUpdate) {
1123
+ if (ctx.state.getResource(address)?.externally_managed) {
1124
+ throw new UserError(`identity.${name} is recorded as an external reference; refusing to modify it remotely.`);
1125
+ }
1126
+ result = await updateIdentity(existingId, name, decl);
1127
+ } else {
1128
+ try {
1129
+ result = await createIdentity(name, decl);
1130
+ } catch (err) {
1131
+ if (!(err instanceof ConflictError)) throw err;
1132
+ if (ctx.createOnly) throw createOnlyExistingResourceError(address);
1133
+ const existing = await provider.findResource("identity", decl.external_id);
1134
+ if (!existing?.id) throw err;
1135
+ result = await updateIdentity(existing.id, name, decl);
1136
+ adopted = true;
1137
+ }
1138
+ }
1139
+ break;
1140
+ }
1141
+ case "channel": {
1142
+ const createChannel = provider.createChannel?.bind(provider);
1143
+ const updateChannel = provider.updateChannel?.bind(provider);
1144
+ if (!createChannel || !updateChannel) {
1145
+ throw new UserError(`Provider '${address.provider}' does not support channels`);
1146
+ }
1147
+ const decl = ctx.config.channels[name];
1148
+ const refs = resolveChannelRefs(name, ctx.config, address.provider, ctx.state);
1149
+ if (isUpdate) {
1150
+ result = await updateChannel(existingId, name, decl, refs);
1151
+ } else {
1152
+ try {
1153
+ result = await createChannel(name, decl, refs);
1154
+ } catch (err) {
1155
+ result = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
1156
+ createOnly: ctx.createOnly,
1157
+ onExisting: (existing) => updateChannel(existing.id, name, decl, refs)
1158
+ });
1159
+ adopted = true;
1160
+ }
1161
+ }
1162
+ break;
1163
+ }
1164
+ case "deployment": {
1165
+ const decl = ctx.config.deployments[name];
1166
+ const refs = resolveDeploymentRefs(name, ctx.config, address.provider, ctx.state);
1167
+ const hasLocalFileSources = decl.resources?.some(
1168
+ (resource) => resource.type === "file" && !resource.file_id && Boolean(resource.source)
1169
+ );
1170
+ const materializeDeployment = async () => {
1171
+ try {
1172
+ return await provider.createDeployment(name, decl, refs, ctx.configPath ?? "");
1173
+ } catch (err) {
1174
+ const preparedFiles = err instanceof DeploymentCreateConflictError ? err.preparedFiles : void 0;
1175
+ const existing = await adoptOnConflict(err, address, provider, ctx.onFeedback, {
1176
+ createOnly: ctx.createOnly,
1177
+ onExisting: (existing2) => provider.updateDeployment(existing2.id, name, decl, refs, ctx.configPath ?? "", preparedFiles)
1178
+ });
1179
+ adopted = true;
1180
+ return existing;
1181
+ }
1182
+ };
1183
+ if (isUpdate && existingId) {
1184
+ result = await provider.updateDeployment(existingId, name, decl, refs, ctx.configPath ?? "");
1185
+ } else {
1186
+ const existing = hasLocalFileSources ? await findExistingByNames(provider, "deployment", [name]) : null;
1187
+ if (existing) {
1188
+ if (ctx.createOnly) throw createOnlyExistingResourceError(address, existing.name);
1189
+ result = await provider.updateDeployment(existing.resource.id, name, decl, refs, ctx.configPath ?? "");
1190
+ emitRuntimeFeedback(ctx.onFeedback, {
1191
+ type: "resource_adopted",
1192
+ level: "info",
1193
+ resource: address,
1194
+ message: `adopt deployment.${name} (${address.provider}) \u2014 already existed remotely as "${existing.name}"`
1195
+ });
1196
+ adopted = true;
1197
+ } else {
1198
+ result = await materializeDeployment();
1199
+ }
1200
+ }
1201
+ break;
1202
+ }
1203
+ case "file": {
1204
+ const decl = ctx.config.files[name];
1205
+ const filePath = resolve4(dirname3(ctx.configPath ?? ""), decl.source);
1206
+ if (isUpdate) {
1207
+ const oldId = ctx.state.getResource(address)?.remote_id;
1208
+ if (oldId) {
1209
+ try {
1210
+ await provider.deleteFile(oldId, priorApiMode);
1211
+ } catch {
1212
+ }
1213
+ }
1214
+ }
1215
+ const info = await provider.uploadFile(
1216
+ filePath,
1217
+ {
1218
+ // Keep the source filename and extension; a declaration label must not
1219
+ // change the multipart filename or the provider's inferred MIME type.
1220
+ name: basename2(filePath),
1221
+ purpose: decl.purpose
1222
+ },
1223
+ apiMode
1224
+ );
1225
+ result = { id: info.id, type: "file" };
1226
+ break;
1227
+ }
1228
+ default:
1229
+ throw new UserError(`Unknown resource type: ${type}`);
1230
+ }
1231
+ const hash = await computeResourceHash(address, ctx.config, ctx.configPath, ctx.state);
1232
+ const comparableHash = computeComparableDesiredHash(address, ctx.config, provider);
1233
+ let remoteHash = comparableHash;
1234
+ let remoteSnapshot;
1235
+ const remote = await readComparableIfSupported(provider, type, result.id, name);
1236
+ if (remote) {
1237
+ remoteHash = contentHash(remote.comparable);
1238
+ remoteSnapshot = remote.snapshot ?? remote.comparable;
1239
+ }
1240
+ const priorResource = ctx.state.getResource(priorAddress);
1241
+ ctx.state.setResource({
1242
+ address,
1243
+ remote_id: result.id,
1244
+ externally_managed: priorResource?.externally_managed || type === "environment" && ctx.config.environments?.[name]?.environment_id || type === "identity" && ctx.config.identities?.[name]?.identity_id ? true : void 0,
1245
+ api_mode: apiMode,
1246
+ version: result.version,
1247
+ content_hash: hash,
1248
+ desired_hash: hash,
1249
+ desired_comparable_hash: remoteHash,
1250
+ desired_readiness_baseline: buildReadinessBaseline(getResourceDeclaration(address, ctx.config)),
1251
+ remote_hash: remoteHash,
1252
+ remote_snapshot: remoteSnapshot,
1253
+ replacement_fingerprint: computeReplacementFingerprint(address, ctx.config),
1254
+ drift_paths: [],
1255
+ drift_status: remoteHash ? "in_sync" : void 0
1256
+ });
1257
+ if (action.previousAddress) ctx.state.removeResource(action.previousAddress);
1258
+ return adopted;
1259
+ }
1260
+ function ownedForwardMemoryStoreIds(ctx, provider) {
1261
+ return ctx.state.listResources().filter(
1262
+ (resource) => resource.address.provider === provider && resource.address.type === "memory_store" && resource.api_mode === "forward" && typeof resource.remote_id === "string"
1263
+ ).map((resource) => resource.remote_id);
1264
+ }
1265
+ function resolveResourceApiMode(type, name, provider, config) {
1266
+ if (provider !== "qoder" || type !== "environment" && type !== "skill" && type !== "vault" && type !== "memory_store" && type !== "file") {
1267
+ return void 0;
1268
+ }
1269
+ if (type === "environment" && config.environments?.[name]?.environment_id) return "auto";
1270
+ let managed = false;
1271
+ let forward = false;
1272
+ for (const agent of Object.values(config.agents ?? {})) {
1273
+ if (agent.provider && agent.provider !== provider) continue;
1274
+ const referenced = type === "environment" ? agent.environment === name : type === "skill" ? agent.skills?.some(
1275
+ (skill) => typeof skill === "string" ? skill === name : skill.type === "custom" && skill.skill_id === name
1276
+ ) : type === "vault" ? agent.vault === name : type === "memory_store" ? agent.memory_stores?.includes(name) : agent.files?.some((file) => (typeof file === "string" ? file : file.file) === name);
1277
+ if (!referenced) continue;
1278
+ if (agent.delivery?.qoder?.type === "forward") forward = true;
1279
+ else managed = true;
1280
+ }
1281
+ if (managed && forward) {
1282
+ throw new UserError(
1283
+ `Qoder ${type}.${name} is referenced by both Managed and Forward agents; declare separate resources for each API domain.`
1284
+ );
1285
+ }
1286
+ return forward ? "forward" : "managed";
1287
+ }
1288
+ async function findExistingByNames(provider, type, names, mode) {
1289
+ for (const candidate of names) {
1290
+ const found = await provider.findResource(type, candidate, void 0, mode);
1291
+ if (found && found.id !== null) return { resource: found, name: candidate };
1292
+ }
1293
+ return null;
1294
+ }
1295
+ async function adoptOnConflict(err, address, provider, onFeedback, opts) {
1296
+ if (!(err instanceof ConflictError)) throw err;
1297
+ const candidates = opts.searchNames?.length ? opts.searchNames : [address.name];
1298
+ if (opts.createOnly) throw createOnlyExistingResourceError(address, candidates.join('" / "'));
1299
+ const existing = await findExistingByNames(provider, address.type, candidates, opts.mode);
1300
+ if (!existing) throw nameReservedError(err, address, candidates.join('" / "'));
1301
+ emitRuntimeFeedback(onFeedback, {
1302
+ type: "resource_adopted",
1303
+ level: "info",
1304
+ resource: address,
1305
+ message: `adopt ${address.type}.${address.name} (${address.provider}) \u2014 already existed remotely as "${existing.name}"`
1306
+ });
1307
+ return opts.onExisting(existing.resource);
1308
+ }
1309
+ function createOnlyExistingResourceError(address, remoteName = address.name) {
1310
+ return new UserError(
1311
+ `Create-only cannot adopt or reconcile existing ${address.type} "${remoteName}" on provider '${address.provider}'. Choose a new name, or use a normal apply/import workflow to manage the existing resource.`
1312
+ );
1313
+ }
1314
+ function nameReservedError(err, address, searchName) {
1315
+ const detail = err instanceof ApiError ? err.message : String(err);
1316
+ if (address.type === "channel" && err instanceof ApiError && err.responseBody.includes("CHANNEL_CREDENTIAL_CONFLICT")) {
1317
+ return new UserError(
1318
+ `${address.provider} rejected channel.${address.name} because its credentials are already used by another Channel. Keep the existing Channel address so it can be updated in place, remove the old Channel first, or use a different credential set. (${detail})`
1319
+ );
1320
+ }
1321
+ return new UserError(
1322
+ `${address.provider} reported ${address.type} "${searchName}" already exists, but it could not be found remotely to adopt. This usually means it was recently deleted and the provider still reserves the name. Wait for the provider to release the name, or use a different name for ${address.type}.${address.name}. (${detail})`
1323
+ );
1324
+ }
1325
+
1326
+ // src/internal/graph/dependency.ts
1327
+ function collectDependencyClosure(graph, roots) {
1328
+ const selected = /* @__PURE__ */ new Map();
1329
+ function visit(address) {
1330
+ const key = addressKey(address);
1331
+ if (selected.has(key)) return;
1332
+ selected.set(key, graph.nodes.get(key) ?? address);
1333
+ for (const dependencyKey of graph.edges.get(key) ?? []) {
1334
+ const dependency = graph.nodes.get(dependencyKey);
1335
+ if (dependency) visit(dependency);
1336
+ }
1337
+ }
1338
+ for (const root of roots) visit(root);
1339
+ return [...selected.values()];
1340
+ }
1341
+ function buildDependencyGraph(config, targetProviders) {
1342
+ const nodes = /* @__PURE__ */ new Map();
1343
+ const edges = /* @__PURE__ */ new Map();
1344
+ function addNode(addr) {
1345
+ const key = addressKey(addr);
1346
+ nodes.set(key, addr);
1347
+ if (!edges.has(key)) edges.set(key, /* @__PURE__ */ new Set());
1348
+ }
1349
+ function addEdge(from, to) {
1350
+ const fromKey = addressKey(from);
1351
+ const toKey = addressKey(to);
1352
+ edges.get(fromKey)?.add(toKey);
1353
+ }
1354
+ for (const provider of targetProviders) {
1355
+ const def = getProvider(provider);
1356
+ const caps = def?.capabilities;
1357
+ if (config.environments) {
1358
+ for (const name of Object.keys(config.environments)) {
1359
+ const decl = config.environments[name];
1360
+ if (decl.provider && decl.provider !== provider) continue;
1361
+ addNode({ type: "environment", name, provider });
1362
+ }
1363
+ }
1364
+ if (config.memory_stores) {
1365
+ for (const name of Object.keys(config.memory_stores)) {
1366
+ const decl = config.memory_stores[name];
1367
+ if (decl.provider && decl.provider !== provider) continue;
1368
+ if (!isSupported(caps, "memory_store")) continue;
1369
+ addNode({ type: "memory_store", name, provider });
1370
+ }
1371
+ }
1372
+ if (config.vaults) {
1373
+ for (const name of Object.keys(config.vaults)) {
1374
+ const decl = config.vaults[name];
1375
+ if (decl.provider && decl.provider !== provider) continue;
1376
+ addNode({ type: "vault", name, provider });
1377
+ }
1378
+ }
1379
+ if (config.skills) {
1380
+ for (const name of Object.keys(config.skills)) {
1381
+ const decl = config.skills[name];
1382
+ if (decl.provider && decl.provider !== provider) continue;
1383
+ addNode({ type: "skill", name, provider });
1384
+ }
1385
+ }
1386
+ if (config.files) {
1387
+ for (const name of Object.keys(config.files)) {
1388
+ const decl = config.files[name];
1389
+ if (decl.provider && decl.provider !== provider) continue;
1390
+ addNode({ type: "file", name, provider });
1391
+ }
1392
+ }
1393
+ if (config.identities && isSupported(caps, "identity")) {
1394
+ for (const name of Object.keys(config.identities)) {
1395
+ const decl = config.identities[name];
1396
+ if (decl.provider && decl.provider !== provider) continue;
1397
+ addNode({ type: "identity", name, provider });
1398
+ }
1399
+ }
1400
+ if (config.agents) {
1401
+ for (const name of Object.keys(config.agents)) {
1402
+ const decl = config.agents[name];
1403
+ if (decl.provider && decl.provider !== provider) continue;
1404
+ const materialization = resolveAgentMaterialization(provider, decl);
1405
+ const agentAddr = { type: materialization.resourceType, name, provider };
1406
+ addNode(agentAddr);
1407
+ if ((decl.default_memory_store || decl.memory_stores?.length) && materialization.resourceType === "template") {
1408
+ const identityName = config.defaults?.identity;
1409
+ if (identityName) {
1410
+ const identityAddr = { type: "identity", name: identityName, provider };
1411
+ if (nodes.has(addressKey(identityAddr))) addEdge(agentAddr, identityAddr);
1412
+ }
1413
+ }
1414
+ if (decl.environment && config.environments?.[decl.environment]) {
1415
+ const envAddr = {
1416
+ type: "environment",
1417
+ name: decl.environment,
1418
+ provider
1419
+ };
1420
+ if (nodes.has(addressKey(envAddr))) {
1421
+ addEdge(agentAddr, envAddr);
1422
+ }
1423
+ }
1424
+ if (decl.skills) {
1425
+ for (const skillRef of decl.skills) {
1426
+ if (typeof skillRef !== "string") continue;
1427
+ const skillName = skillRef;
1428
+ const skillAddr = { type: "skill", name: skillName, provider };
1429
+ if (nodes.has(addressKey(skillAddr))) {
1430
+ addEdge(agentAddr, skillAddr);
1431
+ }
1432
+ }
1433
+ }
1434
+ if (decl.vault) {
1435
+ const vaultAddr = { type: "vault", name: decl.vault, provider };
1436
+ if (nodes.has(addressKey(vaultAddr))) {
1437
+ addEdge(agentAddr, vaultAddr);
1438
+ }
1439
+ }
1440
+ if (decl.files) {
1441
+ for (const file of decl.files) {
1442
+ const fileName = typeof file === "string" ? file : file.file;
1443
+ const fileAddr = { type: "file", name: fileName, provider };
1444
+ if (nodes.has(addressKey(fileAddr))) addEdge(agentAddr, fileAddr);
1445
+ }
1446
+ }
1447
+ if (decl.memory_stores) {
1448
+ for (const msName of decl.memory_stores) {
1449
+ const msAddr = {
1450
+ type: "memory_store",
1451
+ name: msName,
1452
+ provider
1453
+ };
1454
+ if (nodes.has(addressKey(msAddr))) {
1455
+ addEdge(agentAddr, msAddr);
1456
+ }
1457
+ }
1458
+ }
1459
+ if (decl.multiagent && isSupported(caps, "multiagent")) {
1460
+ for (const subName of decl.multiagent.agents) {
1461
+ const subDecl = config.agents[subName];
1462
+ const subType = subDecl ? resolveAgentMaterialization(provider, subDecl).resourceType : "agent";
1463
+ const subAddr = { type: subType, name: subName, provider };
1464
+ addEdge(agentAddr, subAddr);
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ if (config.deployments) {
1470
+ for (const name of Object.keys(config.deployments)) {
1471
+ const decl = config.deployments[name];
1472
+ if (decl.provider && decl.provider !== provider) continue;
1473
+ const depAddr = { type: "deployment", name, provider };
1474
+ addNode(depAddr);
1475
+ const agentDecl = config.agents?.[decl.agent];
1476
+ const agentType = agentDecl ? resolveAgentMaterialization(provider, agentDecl).resourceType : "agent";
1477
+ const agentAddr = { type: agentType, name: decl.agent, provider };
1478
+ if (nodes.has(addressKey(agentAddr))) addEdge(depAddr, agentAddr);
1479
+ if (decl.environment) {
1480
+ const envAddr = { type: "environment", name: decl.environment, provider };
1481
+ if (nodes.has(addressKey(envAddr))) addEdge(depAddr, envAddr);
1482
+ }
1483
+ if (decl.vaults) {
1484
+ for (const vName of decl.vaults) {
1485
+ const vAddr = { type: "vault", name: vName, provider };
1486
+ if (nodes.has(addressKey(vAddr))) addEdge(depAddr, vAddr);
1487
+ }
1488
+ }
1489
+ if (decl.memory_stores) {
1490
+ for (const msName of decl.memory_stores) {
1491
+ const msAddr = { type: "memory_store", name: msName, provider };
1492
+ if (nodes.has(addressKey(msAddr))) addEdge(depAddr, msAddr);
1493
+ }
1494
+ }
1495
+ if (decl.resources) {
1496
+ for (const r of decl.resources) {
1497
+ if (r.type === "memory_store") {
1498
+ const msAddr = { type: "memory_store", name: r.memory_store, provider };
1499
+ if (nodes.has(addressKey(msAddr))) addEdge(depAddr, msAddr);
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+ }
1505
+ if (config.channels && isSupported(caps, "channel")) {
1506
+ for (const name of Object.keys(config.channels)) {
1507
+ const decl = config.channels[name];
1508
+ if (decl.provider && decl.provider !== provider) continue;
1509
+ const channelAddr = { type: "channel", name, provider };
1510
+ addNode(channelAddr);
1511
+ if (decl.mode === "pairing" || !decl.agent) continue;
1512
+ const agentDecl = config.agents?.[decl.agent];
1513
+ const agentType = agentDecl ? resolveAgentMaterialization(provider, agentDecl).resourceType : "agent";
1514
+ const agentAddr = { type: agentType, name: decl.agent, provider };
1515
+ if (nodes.has(addressKey(agentAddr))) addEdge(channelAddr, agentAddr);
1516
+ const identityName = decl.identity ?? config.defaults?.identity;
1517
+ if (identityName) {
1518
+ const identityAddr = { type: "identity", name: identityName, provider };
1519
+ if (nodes.has(addressKey(identityAddr))) addEdge(channelAddr, identityAddr);
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+ return { nodes, edges };
1525
+ }
1526
+ function topologicalSort(graph) {
1527
+ const visited = /* @__PURE__ */ new Set();
1528
+ const sorted = [];
1529
+ const visiting = /* @__PURE__ */ new Set();
1530
+ function visit(key) {
1531
+ if (visited.has(key)) return;
1532
+ if (visiting.has(key)) {
1533
+ throw new UserError(`Circular dependency detected involving: ${key}`);
1534
+ }
1535
+ visiting.add(key);
1536
+ const deps = graph.edges.get(key) ?? /* @__PURE__ */ new Set();
1537
+ for (const dep of deps) {
1538
+ visit(dep);
1539
+ }
1540
+ visiting.delete(key);
1541
+ visited.add(key);
1542
+ sorted.push(graph.nodes.get(key));
1543
+ }
1544
+ for (const key of graph.nodes.keys()) {
1545
+ visit(key);
1546
+ }
1547
+ return sorted;
1548
+ }
1549
+
1550
+ // src/internal/planner/planner.ts
1551
+ async function buildPlan(config, state, options = {}) {
1552
+ const scopedConfig = options.resourceAddresses ? selectProjectConfig(config, options.resourceAddresses) : config;
1553
+ const resourceKeys = options.resourceAddresses ? new Set(options.resourceAddresses.map(addressKey)) : void 0;
1554
+ const scopedState = resourceKeys ? { ...state, resources: state.resources.filter((resource) => resourceKeys.has(addressKey(resource.address))) } : state;
1555
+ const diagnostics = new DiagnosticCollector();
1556
+ const actions = [];
1557
+ const targetProviders = options.providers ?? resolveTargetProviders(scopedConfig);
1558
+ collectReferenceDiagnostics(scopedConfig, diagnostics);
1559
+ collectProviderCapabilities(scopedConfig, targetProviders, diagnostics);
1560
+ const graph = buildDependencyGraph(scopedConfig, targetProviders);
1561
+ const sorted = topologicalSort(graph);
1562
+ const stateIndex = /* @__PURE__ */ new Map();
1563
+ for (const res of scopedState.resources) {
1564
+ stateIndex.set(addressKey(res.address), res);
1565
+ }
1566
+ const remoteIdLookup = /* @__PURE__ */ new Map();
1567
+ for (const res of scopedState.resources) {
1568
+ remoteIdLookup.set(addressKey(res.address), res);
1569
+ }
1570
+ const hashStateLookup = { getResource: (addr) => remoteIdLookup.get(addressKey(addr)) };
1571
+ for (const address of sorted) {
1572
+ const key = addressKey(address);
1573
+ const desiredHash = await computeResourceHash(address, config, options.configPath, hashStateLookup);
1574
+ const existing = stateIndex.get(key);
1575
+ const deps = getDependencies(address, graph);
1576
+ const needsNativeDeploymentMaterialization = address.type === "deployment" && existing?.remote_id === null && getProvider(address.provider)?.capabilities.deployment.tier === "native";
1577
+ if (address.type === "environment" && existing) {
1578
+ const envDecl = config.environments?.[address.name];
1579
+ if (existing.externally_managed && envDecl && !envDecl.environment_id) {
1580
+ diagnostics.error(
1581
+ "plan.environment.ownership_transition",
1582
+ `environment.${address.name} is recorded as an external reference (${existing.remote_id ?? "unknown id"}); removing 'environment_id' would make OpenCMA modify and eventually delete a remote environment it does not own. Restore 'environment_id' to keep it as a reference, or release it first with 'agents state rm environment.${address.name}' (then 'agents state import' to adopt the remote as a managed resource).`,
1583
+ address
1584
+ );
1585
+ stateIndex.delete(key);
1586
+ continue;
1587
+ }
1588
+ if (!existing.externally_managed && existing.remote_id && envDecl?.environment_id && envDecl.environment_id !== existing.remote_id) {
1589
+ diagnostics.warning(
1590
+ "plan.environment.ownership_orphan",
1591
+ `environment.${address.name}: switching to external reference '${envDecl.environment_id}' orphans the previously managed remote environment '${existing.remote_id}' \u2014 it will no longer be tracked or deletable by OpenCMA.`,
1592
+ address
1593
+ );
1594
+ }
1595
+ }
1596
+ if (address.type === "identity" && existing) {
1597
+ const identityDecl = config.identities?.[address.name];
1598
+ if (existing.externally_managed && identityDecl && !identityDecl.identity_id) {
1599
+ diagnostics.error(
1600
+ "plan.identity.ownership_transition",
1601
+ `identity.${address.name} is recorded as an external reference (${existing.remote_id ?? "unknown id"}); replacing identity_id with a managed declaration would modify and eventually delete an Identity this project does not own. Restore identity_id or release the state reference first.`,
1602
+ address
1603
+ );
1604
+ stateIndex.delete(key);
1605
+ continue;
1606
+ }
1607
+ if (!existing.externally_managed && existing.remote_id && identityDecl?.identity_id && identityDecl.identity_id !== existing.remote_id) {
1608
+ diagnostics.warning(
1609
+ "plan.identity.ownership_orphan",
1610
+ `identity.${address.name}: switching to external reference '${identityDecl.identity_id}' orphans the previously managed Identity '${existing.remote_id}'.`,
1611
+ address
1612
+ );
1613
+ }
1614
+ }
1615
+ const isExternalReference = address.type === "environment" && Boolean(config.environments?.[address.name]?.environment_id) || address.type === "identity" && Boolean(config.identities?.[address.name]?.identity_id);
1616
+ const createReason = isExternalReference ? `Record external ${address.type} reference (no remote mutation)` : "Resource does not exist in state";
1617
+ const updateSuffix = isExternalReference ? " \u2014 external reference, no remote mutation" : "";
1618
+ if (!existing) {
1619
+ actions.push({
1620
+ action: "create",
1621
+ address,
1622
+ driftKind: "none",
1623
+ readinessImpact: "blocking",
1624
+ reason: createReason,
1625
+ after: { content_hash: desiredHash },
1626
+ dependencies: deps
1627
+ });
1628
+ } else if (needsNativeDeploymentMaterialization) {
1629
+ actions.push({
1630
+ action: "update",
1631
+ address,
1632
+ driftKind: "none",
1633
+ readinessImpact: "blocking",
1634
+ reason: "Materialize legacy state as a native deployment",
1635
+ before: { content_hash: existing.desired_hash ?? existing.content_hash },
1636
+ after: { content_hash: desiredHash },
1637
+ dependencies: deps
1638
+ });
1639
+ } else if ((existing.desired_hash ?? existing.content_hash) !== desiredHash && existing.drift_status === "drifted") {
1640
+ const changedPaths = collectChangedPaths(address, config, existing, true);
1641
+ actions.push({
1642
+ action: "update",
1643
+ address,
1644
+ driftKind: "both",
1645
+ readinessImpact: classifyReadinessImpact("update", changedPaths),
1646
+ changedPaths,
1647
+ reason: `Local config changed and remote drift detected${updateSuffix}`,
1648
+ before: {
1649
+ content_hash: existing.desired_hash ?? existing.content_hash,
1650
+ remote_hash: existing.remote_hash,
1651
+ drift_status: existing.drift_status
1652
+ },
1653
+ after: { content_hash: desiredHash },
1654
+ dependencies: deps
1655
+ });
1656
+ } else if ((existing.desired_hash ?? existing.content_hash) !== desiredHash) {
1657
+ const changedPaths = collectChangedPaths(address, config, existing, false);
1658
+ actions.push({
1659
+ action: "update",
1660
+ address,
1661
+ driftKind: "local",
1662
+ readinessImpact: classifyReadinessImpact("update", changedPaths),
1663
+ changedPaths,
1664
+ reason: `Local config changed${updateSuffix}`,
1665
+ before: { content_hash: existing.desired_hash ?? existing.content_hash },
1666
+ after: { content_hash: desiredHash },
1667
+ dependencies: deps
1668
+ });
1669
+ } else if (existing.drift_status === "drifted") {
1670
+ const changedPaths = existing.drift_paths;
1671
+ actions.push({
1672
+ action: "update",
1673
+ address,
1674
+ driftKind: "remote",
1675
+ readinessImpact: classifyReadinessImpact("update", changedPaths),
1676
+ changedPaths,
1677
+ reason: `Remote drift detected${updateSuffix}`,
1678
+ before: {
1679
+ content_hash: existing.desired_hash ?? existing.content_hash,
1680
+ remote_hash: existing.remote_hash,
1681
+ drift_status: existing.drift_status
1682
+ },
1683
+ after: { content_hash: desiredHash },
1684
+ dependencies: deps
1685
+ });
1686
+ } else {
1687
+ actions.push({
1688
+ action: "no-op",
1689
+ address,
1690
+ driftKind: "none",
1691
+ readinessImpact: "none",
1692
+ reason: existing.drift_status === "unchecked" ? "No changes detected (remote content drift unchecked)" : "No changes detected",
1693
+ dependencies: deps
1694
+ });
1695
+ }
1696
+ stateIndex.delete(key);
1697
+ }
1698
+ const toDelete = Array.from(stateIndex.values()).reverse();
1699
+ for (const res of toDelete) {
1700
+ const replacement = deliveryReplacementAddress(res.address, graph);
1701
+ actions.push({
1702
+ action: "delete",
1703
+ address: res.address,
1704
+ driftKind: "none",
1705
+ readinessImpact: "blocking",
1706
+ reason: res.externally_managed ? "Remove local reference only \u2014 externally managed remote resource is left intact" : "Resource removed from configuration",
1707
+ before: { content_hash: res.desired_hash ?? res.content_hash },
1708
+ dependencies: replacement ? [replacement] : []
1709
+ });
1710
+ }
1711
+ coalesceChannelRenames(actions, scopedConfig, scopedState);
1712
+ return { actions, diagnostics: diagnostics.getAll() };
1713
+ }
1714
+ function selectProjectConfig(config, addresses) {
1715
+ const providers = new Set(addresses.map((address) => address.provider));
1716
+ const namesByType = /* @__PURE__ */ new Map();
1717
+ for (const address of addresses) {
1718
+ const names = namesByType.get(address.type) ?? /* @__PURE__ */ new Set();
1719
+ names.add(address.name);
1720
+ namesByType.set(address.type, names);
1721
+ }
1722
+ const pick = (record, names) => {
1723
+ if (!record || !names?.size) return void 0;
1724
+ const selected = Object.fromEntries(Object.entries(record).filter(([name]) => names.has(name)));
1725
+ return Object.keys(selected).length > 0 ? selected : void 0;
1726
+ };
1727
+ const agentNames = /* @__PURE__ */ new Set([...namesByType.get("agent") ?? [], ...namesByType.get("template") ?? []]);
1728
+ const selectedAgents = pick(config.agents, agentNames);
1729
+ const tunnelNames = /* @__PURE__ */ new Set();
1730
+ for (const agent of Object.values(selectedAgents ?? {})) {
1731
+ if (agent.tunnel) tunnelNames.add(agent.tunnel);
1732
+ }
1733
+ for (const deployment of Object.values(pick(config.deployments, namesByType.get("deployment")) ?? {})) {
1734
+ if (deployment.tunnel) tunnelNames.add(deployment.tunnel);
1735
+ }
1736
+ const identityNames = namesByType.get("identity");
1737
+ const selectedProviders = Object.fromEntries(
1738
+ Object.entries(config.providers).filter(([providerName]) => providers.has(providerName))
1739
+ );
1740
+ const defaultProvider = providers.size === 1 ? [...providers][0] : config.defaults?.provider;
1741
+ const defaultIdentity = config.defaults?.identity && identityNames?.has(config.defaults.identity) ? config.defaults.identity : void 0;
1742
+ return {
1743
+ ...config,
1744
+ providers: selectedProviders,
1745
+ defaults: defaultProvider || defaultIdentity ? { provider: defaultProvider, identity: defaultIdentity } : void 0,
1746
+ environments: pick(config.environments, namesByType.get("environment")),
1747
+ tunnels: pick(config.tunnels, tunnelNames),
1748
+ vaults: pick(config.vaults, namesByType.get("vault")),
1749
+ memory_stores: pick(config.memory_stores, namesByType.get("memory_store")),
1750
+ skills: pick(config.skills, namesByType.get("skill")),
1751
+ files: pick(config.files, namesByType.get("file")),
1752
+ identities: pick(config.identities, identityNames),
1753
+ agents: selectedAgents,
1754
+ channels: pick(config.channels, namesByType.get("channel")),
1755
+ deployments: pick(config.deployments, namesByType.get("deployment"))
1756
+ };
1757
+ }
1758
+ function coalesceChannelRenames(actions, config, state) {
1759
+ const creates = actions.filter((action) => action.action === "create" && action.address.type === "channel");
1760
+ const deletes = actions.filter((action) => action.action === "delete" && action.address.type === "channel");
1761
+ const stateByAddress = new Map(state.resources.map((resource) => [addressKey(resource.address), resource]));
1762
+ const matchedDeletes = /* @__PURE__ */ new Set();
1763
+ for (const create of creates) {
1764
+ const desiredType = config.channels?.[create.address.name]?.type;
1765
+ if (!desiredType) continue;
1766
+ const desiredFingerprint = computeReplacementFingerprint(create.address, config);
1767
+ const candidates = deletes.filter((deletion2) => {
1768
+ if (matchedDeletes.has(deletion2) || deletion2.address.provider !== create.address.provider) return false;
1769
+ const prior2 = stateByAddress.get(addressKey(deletion2.address));
1770
+ const snapshot = prior2?.remote_snapshot;
1771
+ if (snapshot?.channel_type !== desiredType) return false;
1772
+ return !prior2?.replacement_fingerprint || prior2.replacement_fingerprint === desiredFingerprint;
1773
+ });
1774
+ if (candidates.length !== 1) continue;
1775
+ const deletion = candidates[0];
1776
+ const prior = stateByAddress.get(addressKey(deletion.address));
1777
+ const competingCreates = creates.filter(
1778
+ (candidate) => candidate !== create && candidate.address.provider === create.address.provider && config.channels?.[candidate.address.name]?.type === desiredType && (!prior?.replacement_fingerprint || computeReplacementFingerprint(candidate.address, config) === prior.replacement_fingerprint)
1779
+ );
1780
+ if (competingCreates.length > 0) continue;
1781
+ create.action = "update";
1782
+ create.previousAddress = deletion.address;
1783
+ create.before = deletion.before;
1784
+ create.driftKind = "local";
1785
+ create.reason = `Channel key renamed from '${deletion.address.name}' (remote resource retained)`;
1786
+ protectRenamedChannelDependencies(actions, stateByAddress, deletion, create);
1787
+ matchedDeletes.add(deletion);
1788
+ }
1789
+ for (let index = actions.length - 1; index >= 0; index--) {
1790
+ if (matchedDeletes.has(actions[index])) actions.splice(index, 1);
1791
+ }
1792
+ }
1793
+ function protectRenamedChannelDependencies(actions, stateByAddress, deletion, replacement) {
1794
+ const prior = stateByAddress.get(addressKey(deletion.address));
1795
+ const snapshot = prior?.remote_snapshot;
1796
+ const referencedIds = new Set(
1797
+ [snapshot?.identity_id, snapshot?.template_id].filter((id) => typeof id === "string")
1798
+ );
1799
+ if (referencedIds.size === 0) return;
1800
+ for (const action of actions) {
1801
+ if (action.action !== "delete" || action.address.type !== "identity" && action.address.type !== "template" || action.address.provider !== replacement.address.provider) {
1802
+ continue;
1803
+ }
1804
+ const dependency = stateByAddress.get(addressKey(action.address));
1805
+ if (!dependency?.remote_id || !referencedIds.has(dependency.remote_id)) continue;
1806
+ if (!action.dependencies.some((address) => addressKey(address) === addressKey(replacement.address))) {
1807
+ action.dependencies.push(replacement.address);
1808
+ }
1809
+ }
1810
+ }
1811
+ function deliveryReplacementAddress(address, graph) {
1812
+ if (address.type !== "agent" && address.type !== "template") return void 0;
1813
+ const replacementType = address.type === "agent" ? "template" : "agent";
1814
+ const candidate = { ...address, type: replacementType };
1815
+ return graph.nodes.has(addressKey(candidate)) ? candidate : void 0;
1816
+ }
1817
+ function collectChangedPaths(address, config, existing, includeRemote) {
1818
+ const current = buildReadinessBaseline(getResourceDeclaration(address, config));
1819
+ const localPaths = existing.desired_readiness_baseline ? diffReadinessBaseline(existing.desired_readiness_baseline, current) : void 0;
1820
+ if (!includeRemote) return localPaths;
1821
+ if (!localPaths && !existing.drift_paths) return void 0;
1822
+ return [.../* @__PURE__ */ new Set([...localPaths ?? [], ...existing.drift_paths ?? []])].sort();
1823
+ }
1824
+ function getDependencies(address, graph) {
1825
+ const key = addressKey(address);
1826
+ const depKeys = graph.edges.get(key) ?? /* @__PURE__ */ new Set();
1827
+ return Array.from(depKeys).map((k) => graph.nodes.get(k)).filter((n) => n !== void 0);
1828
+ }
1829
+
1830
+ // src/internal/planner/refresh.ts
1831
+ async function refreshState(state, providers, options = {}) {
1832
+ const resources = state.listResources();
1833
+ const removed = [];
1834
+ const errors = [];
1835
+ let dirty = false;
1836
+ for (const res of resources) {
1837
+ if (options.resourceKeys && !options.resourceKeys.has(addressKey(res.address))) {
1838
+ continue;
1839
+ }
1840
+ if (options.targetProviders && !options.targetProviders.includes(res.address.provider)) {
1841
+ continue;
1842
+ }
1843
+ const provider = providers.get(res.address.provider);
1844
+ if (!provider) continue;
1845
+ try {
1846
+ const support = provider.getDriftSupport?.(res.address.type) ?? "existence";
1847
+ if (supportsFullDrift(provider, res.address.type) && provider.normalizeDesiredResource) {
1848
+ const remote2 = await provider.readComparableResource?.(res.address.type, res.remote_id, res.address.name);
1849
+ if (!remote2) {
1850
+ if (!options.quiet) {
1851
+ emitRuntimeFeedback(options.onFeedback, {
1852
+ type: "refresh_resource_missing",
1853
+ level: "warning",
1854
+ resource: res.address,
1855
+ message: `${res.address.type}.${res.address.name} (${res.address.provider}) \u2014 not found remotely, will recreate`
1856
+ });
1857
+ }
1858
+ state.removeResource(res.address);
1859
+ removed.push(res);
1860
+ dirty = true;
1861
+ continue;
1862
+ }
1863
+ const remoteHash = contentHash(remote2.comparable);
1864
+ const decl = options.config ? getResourceDeclaration(res.address, options.config) : null;
1865
+ const desiredComparable = decl ? provider.normalizeDesiredResource(res.address.type, res.address.name, decl) : null;
1866
+ const desiredComparableHash = desiredComparable === null ? void 0 : contentHash(desiredComparable);
1867
+ const baselineHash = res.desired_comparable_hash ?? desiredComparableHash ?? remoteHash;
1868
+ const driftStatus = baselineHash && remoteHash !== baselineHash ? "drifted" : "in_sync";
1869
+ const comparisonBaseline = res.remote_snapshot ?? desiredComparable;
1870
+ const driftPaths = driftStatus === "drifted" && comparisonBaseline != null ? diffChangedPaths(comparisonBaseline, remote2.comparable) : [];
1871
+ state.setResource({
1872
+ ...res,
1873
+ version: remote2.version ?? res.version,
1874
+ remote_id: remote2.id,
1875
+ desired_hash: res.desired_hash ?? res.content_hash,
1876
+ desired_comparable_hash: baselineHash,
1877
+ remote_hash: remoteHash,
1878
+ remote_snapshot: remote2.snapshot ?? remote2.comparable,
1879
+ drift_paths: driftPaths,
1880
+ drift_status: driftStatus
1881
+ });
1882
+ dirty = true;
1883
+ continue;
1884
+ }
1885
+ if (support === "unsupported") {
1886
+ if (!options.quiet) {
1887
+ emitRuntimeFeedback(options.onFeedback, {
1888
+ type: "refresh_drift_unchecked",
1889
+ level: "warning",
1890
+ resource: res.address,
1891
+ message: `Content drift not checked for ${res.address.type}.${res.address.name} (${res.address.provider}): unsupported`
1892
+ });
1893
+ }
1894
+ state.setResource({
1895
+ ...res,
1896
+ desired_hash: res.desired_hash ?? res.content_hash,
1897
+ drift_status: "unchecked"
1898
+ });
1899
+ dirty = true;
1900
+ continue;
1901
+ }
1902
+ const remote = await provider.findResource(res.address.type, res.address.name, res.remote_id, res.api_mode);
1903
+ if (!remote) {
1904
+ if (!options.quiet) {
1905
+ emitRuntimeFeedback(options.onFeedback, {
1906
+ type: "refresh_resource_missing",
1907
+ level: "warning",
1908
+ resource: res.address,
1909
+ message: `${res.address.type}.${res.address.name} (${res.address.provider}) \u2014 not found remotely, will recreate`
1910
+ });
1911
+ }
1912
+ state.removeResource(res.address);
1913
+ removed.push(res);
1914
+ dirty = true;
1915
+ } else {
1916
+ if (!options.quiet) {
1917
+ emitRuntimeFeedback(options.onFeedback, {
1918
+ type: "refresh_drift_unchecked",
1919
+ level: "warning",
1920
+ resource: res.address,
1921
+ message: `Content drift not checked for ${res.address.type}.${res.address.name} (${res.address.provider}): existence-only`
1922
+ });
1923
+ }
1924
+ state.setResource({
1925
+ ...res,
1926
+ version: remote.version ?? res.version,
1927
+ remote_id: remote.id,
1928
+ desired_hash: res.desired_hash ?? res.content_hash,
1929
+ drift_status: "unchecked"
1930
+ });
1931
+ dirty = true;
1932
+ }
1933
+ } catch (err) {
1934
+ const error = err instanceof Error ? err : new Error(String(err));
1935
+ if (!options.quiet) {
1936
+ emitRuntimeFeedback(options.onFeedback, {
1937
+ type: "refresh_resource_failed",
1938
+ level: "warning",
1939
+ resource: res.address,
1940
+ message: `Failed to refresh ${res.address.type}.${res.address.name} (${res.address.provider}): ${error.message}`
1941
+ });
1942
+ }
1943
+ errors.push({ resource: res, error });
1944
+ }
1945
+ }
1946
+ if (dirty) {
1947
+ await state.save();
1948
+ }
1949
+ return { removed, errors };
1950
+ }
1951
+
1952
+ // src/internal/core/resource-runtime.ts
1953
+ async function planProjectWithStateBackend(input, options = {}) {
1954
+ return readProjectRuntime(input, (ctx) => planProjectContext(ctx, options));
1955
+ }
1956
+ async function syncProjectResourcesWithStateBackend(input, options = {}) {
1957
+ return writeProjectRuntime(input, async (ctx) => {
1958
+ const planned = await planProjectContext(ctx, options);
1959
+ if (options.refreshOnly) {
1960
+ return { planned };
1961
+ }
1962
+ if (options.mode === "create-only") {
1963
+ const errorDiagnostic = planned.plan.diagnostics.find((diagnostic) => diagnostic.severity === "error");
1964
+ if (errorDiagnostic) throw new UserError(errorDiagnostic.message);
1965
+ }
1966
+ return {
1967
+ planned,
1968
+ execution: await executePlannedProject(planned, {
1969
+ onFeedback: options.onFeedback,
1970
+ policy: options.policy,
1971
+ confirm: options.confirm,
1972
+ concurrency: options.concurrency
1973
+ })
1974
+ };
1975
+ });
1976
+ }
1977
+ var IMPORTABLE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
1978
+ "environment",
1979
+ "vault",
1980
+ "memory_store",
1981
+ "skill",
1982
+ "agent",
1983
+ "template",
1984
+ "identity",
1985
+ "channel"
1986
+ ]);
1987
+ async function importResource(ctx, address, remoteId, options = {}) {
1988
+ if (!IMPORTABLE_RESOURCE_TYPES.has(address.type)) {
1989
+ throw new UserError(
1990
+ `Invalid resource type: ${address.type}. Valid types: ${[...IMPORTABLE_RESOURCE_TYPES].join(", ")}`
1991
+ );
1992
+ }
1993
+ if (ctx.state.getResource(address)) {
1994
+ throw new UserError(
1995
+ `Resource ${address.provider}.${address.type}.${address.name} already exists in state. Remove it before re-importing.`
1996
+ );
1997
+ }
1998
+ const planned = await planProjectContext(ctx, {
1999
+ provider: address.provider,
2000
+ refresh: false,
2001
+ quiet: true
2002
+ });
2003
+ const action = planned.plan.actions.find(
2004
+ (item) => item.address.provider === address.provider && item.address.type === address.type && item.address.name === address.name
2005
+ );
2006
+ if (action?.action !== "create") {
2007
+ throw new UserError(`Resource ${address.type}.${address.name} is not declared in the project config.`);
2008
+ }
2009
+ const contentHash2 = action.after?.content_hash;
2010
+ if (!contentHash2) {
2011
+ throw new UserError(`Planned ${address.type}.${address.name} is missing a content hash.`);
2012
+ }
2013
+ const provider = ctx.providers.get(address.provider);
2014
+ const remote = provider ? await readComparableIfSupported(provider, address.type, remoteId, address.name) : null;
2015
+ const remoteHash = remote ? contentHash(remote.comparable) : void 0;
2016
+ const resource = {
2017
+ address,
2018
+ remote_id: remoteId,
2019
+ externally_managed: address.type === "environment" && ctx.config.environments?.[address.name]?.environment_id ? true : void 0,
2020
+ version: options.resourceVersion ?? remote?.version,
2021
+ content_hash: contentHash2,
2022
+ desired_hash: contentHash2,
2023
+ desired_comparable_hash: remoteHash,
2024
+ desired_readiness_baseline: buildReadinessBaseline(getResourceDeclaration(address, ctx.config)),
2025
+ remote_hash: remoteHash,
2026
+ remote_snapshot: remote ? remote.snapshot ?? remote.comparable : void 0,
2027
+ drift_status: remoteHash ? "in_sync" : void 0
2028
+ };
2029
+ ctx.state.setResource(resource);
2030
+ await ctx.state.save();
2031
+ return resource;
2032
+ }
2033
+ async function planProjectContext(ctx, options = {}) {
2034
+ if (options.mode === "create-only" && !options.scope) {
2035
+ throw new UserError("Resource create-only mode requires an explicit resource scope.");
2036
+ }
2037
+ let targetProviders = resolveTargetProviders2(options.provider);
2038
+ if (!targetProviders && options.scope) {
2039
+ targetProviders = [...new Set(options.scope.roots.map((root) => root.provider))];
2040
+ }
2041
+ const selectedAddresses = options.scope ? resolvePlanScope(ctx, targetProviders ?? [], options.scope) : void 0;
2042
+ const resourceKeys = selectedAddresses ? new Set(selectedAddresses.map(addressKey)) : void 0;
2043
+ const refreshResult = options.refresh !== false && ctx.state.listResources().length > 0 ? await refreshState(ctx.state, ctx.providers, {
2044
+ targetProviders,
2045
+ resourceKeys,
2046
+ config: ctx.config,
2047
+ quiet: options.quiet ?? true,
2048
+ onFeedback: options.onFeedback
2049
+ }) : void 0;
2050
+ let plan = await buildPlan(ctx.config, ctx.state.getStateFile(), {
2051
+ providers: targetProviders,
2052
+ configPath: ctx.configPath,
2053
+ resourceAddresses: selectedAddresses
2054
+ });
2055
+ if (options.mode === "create-only" && options.scope) {
2056
+ plan = enforceCreateOnlyPlan(plan, options.scope, toResourceRefreshResult(refreshResult));
2057
+ }
2058
+ return {
2059
+ executionContext: ctx,
2060
+ plan,
2061
+ refreshResult: toResourceRefreshResult(refreshResult),
2062
+ targetProviders,
2063
+ destructiveActions: selectDestructive(plan.actions),
2064
+ selectedAddresses,
2065
+ mode: options.mode
2066
+ };
2067
+ }
2068
+ function enforceCreateOnlyPlan(plan, scope, refreshResult) {
2069
+ const reasons = [];
2070
+ const rootKeys = new Set(scope.roots.map(addressKey));
2071
+ const refreshError = refreshResult?.errors[0];
2072
+ if (refreshError) {
2073
+ reasons.push(
2074
+ `Cannot verify scoped dependencies because refresh failed for ${addressKey(refreshError.resource.address)}: ${refreshError.error}`
2075
+ );
2076
+ }
2077
+ for (const root of scope.roots) {
2078
+ const rootKey = addressKey(root);
2079
+ const rootAction = plan.actions.find((action) => addressKey(action.address) === rootKey);
2080
+ if (!rootAction) {
2081
+ reasons.push(`Scoped plan did not contain target resource ${rootKey}.`);
2082
+ } else if (rootAction.action !== "create") {
2083
+ reasons.push(`Target resource ${rootKey} must be new, but the scoped plan requires '${rootAction.action}'.`);
2084
+ }
2085
+ }
2086
+ const dependencyChanges = plan.actions.filter(
2087
+ (action) => !rootKeys.has(addressKey(action.address)) && action.action !== "no-op"
2088
+ );
2089
+ if (dependencyChanges.length > 0) {
2090
+ const labels = dependencyChanges.map((action) => `${addressKey(action.address)} (${action.action})`).join(", ");
2091
+ reasons.push(`Create-only requires every scoped dependency to be up-to-date. Reconcile first: ${labels}.`);
2092
+ }
2093
+ if (reasons.length === 0) return plan;
2094
+ return {
2095
+ ...plan,
2096
+ diagnostics: [
2097
+ ...plan.diagnostics,
2098
+ {
2099
+ severity: "error",
2100
+ code: "resource.create_only.blocked",
2101
+ message: reasons.join(" "),
2102
+ resource: scope.roots[0]
2103
+ }
2104
+ ]
2105
+ };
2106
+ }
2107
+ function resolvePlanScope(ctx, targetProviders, scope) {
2108
+ if (scope.roots.length === 0) {
2109
+ throw new UserError("Resource plan scope requires at least one root address.");
2110
+ }
2111
+ for (const root of scope.roots) {
2112
+ if (!targetProviders.includes(root.provider)) {
2113
+ throw new UserError(
2114
+ `Scoped resource ${addressKey(root)} is outside the selected provider set: ${targetProviders.join(", ")}.`
2115
+ );
2116
+ }
2117
+ }
2118
+ const graph = buildDependencyGraph(ctx.config, targetProviders);
2119
+ for (const root of scope.roots) {
2120
+ if (!graph.nodes.has(addressKey(root))) {
2121
+ throw new UserError(`Scoped resource ${addressKey(root)} is not declared in the project config.`);
2122
+ }
2123
+ }
2124
+ return scope.includeDependencies === false ? [...scope.roots] : collectDependencyClosure(graph, scope.roots);
2125
+ }
2126
+ async function executePlannedProject(planned, options = {}) {
2127
+ if (planned.mode === "create-only") {
2128
+ const errorDiagnostic = planned.plan.diagnostics.find((diagnostic) => diagnostic.severity === "error");
2129
+ if (errorDiagnostic) throw new UserError(errorDiagnostic.message);
2130
+ }
2131
+ const decision = await decideDestructive(planned.destructiveActions, {
2132
+ policy: options.policy,
2133
+ confirm: options.confirm
2134
+ });
2135
+ if (decision !== "proceed") {
2136
+ throw new UserError(
2137
+ decision === "cancelled" ? "Destructive actions were declined. No remote resources were changed." : "Current plan contains destructive actions. Apply will not delete remote resources."
2138
+ );
2139
+ }
2140
+ const ctx = planned.executionContext;
2141
+ const execution = await executePlan(
2142
+ planned.plan,
2143
+ {
2144
+ config: ctx.config,
2145
+ configPath: ctx.configPath,
2146
+ providers: ctx.providers,
2147
+ state: ctx.state,
2148
+ onFeedback: options.onFeedback,
2149
+ createOnly: planned.mode === "create-only"
2150
+ },
2151
+ { concurrency: options.concurrency }
2152
+ );
2153
+ return toResourceExecutionResult(execution);
2154
+ }
2155
+ function resolveTargetProviders2(provider) {
2156
+ if (!provider || provider === "all") return void 0;
2157
+ return [provider];
2158
+ }
2159
+ function selectDestructive(actions) {
2160
+ return actions.filter((action) => action.action === "delete");
2161
+ }
2162
+ async function decideDestructive(destructiveActions, options = {}) {
2163
+ if (destructiveActions.length === 0) return "proceed";
2164
+ switch (options.policy ?? "block") {
2165
+ case "force":
2166
+ return "proceed";
2167
+ case "prompt":
2168
+ if (!options.confirm) return "blocked";
2169
+ return await options.confirm(destructiveActions) ? "proceed" : "cancelled";
2170
+ default:
2171
+ return "blocked";
2172
+ }
2173
+ }
2174
+ function toResourceRefreshResult(result) {
2175
+ if (!result) return void 0;
2176
+ return {
2177
+ removed: result.removed,
2178
+ errors: result.errors.map((item) => ({
2179
+ resource: item.resource,
2180
+ error: item.error.message
2181
+ }))
2182
+ };
2183
+ }
2184
+ function toResourceExecutionResult(result) {
2185
+ return {
2186
+ results: result.results.map((item) => ({
2187
+ action: item.action,
2188
+ status: item.status,
2189
+ error: item.error?.message
2190
+ })),
2191
+ partial: result.partial
2192
+ };
2193
+ }
2194
+
2195
+ // src/internal/state/state-manager.ts
2196
+ import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
2197
+ import { dirname as dirname4 } from "path";
2198
+ var StateManager = class _StateManager {
2199
+ state;
2200
+ path;
2201
+ index;
2202
+ constructor(state, path) {
2203
+ this.state = state;
2204
+ this.path = path;
2205
+ this.index = this.buildIndex();
2206
+ }
2207
+ buildIndex() {
2208
+ const idx = /* @__PURE__ */ new Map();
2209
+ for (let i = 0; i < this.state.resources.length; i++) {
2210
+ idx.set(addressKey(this.state.resources[i].address), i);
2211
+ }
2212
+ return idx;
2213
+ }
2214
+ static async load(path) {
2215
+ try {
2216
+ const data = JSON.parse(await readFile(path, "utf8"));
2217
+ const raw = data.resources ?? [];
2218
+ const resources = raw.map((r) => ({
2219
+ address: r.address,
2220
+ remote_id: r.remote_id,
2221
+ externally_managed: r.externally_managed === true ? true : void 0,
2222
+ api_mode: r.api_mode === "forward" ? "forward" : r.api_mode === "managed" ? "managed" : void 0,
2223
+ version: r.version,
2224
+ content_hash: r.content_hash ?? r.desired_hash ?? "",
2225
+ desired_hash: r.desired_hash ?? r.content_hash ?? "",
2226
+ desired_comparable_hash: r.desired_comparable_hash,
2227
+ desired_readiness_baseline: r.desired_readiness_baseline,
2228
+ remote_hash: r.remote_hash,
2229
+ remote_snapshot: r.remote_snapshot,
2230
+ replacement_fingerprint: r.replacement_fingerprint,
2231
+ drift_paths: r.drift_paths,
2232
+ drift_status: r.drift_status
2233
+ }));
2234
+ const pending = Array.isArray(data.pending_default_memory_store_cleanups) ? data.pending_default_memory_store_cleanups : void 0;
2235
+ return new _StateManager(
2236
+ { resources, ...pending ? { pending_default_memory_store_cleanups: pending } : {} },
2237
+ path
2238
+ );
2239
+ } catch (err) {
2240
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
2241
+ return _StateManager.initialize(path);
2242
+ }
2243
+ throw err;
2244
+ }
2245
+ }
2246
+ static initialize(path) {
2247
+ return new _StateManager({ resources: [] }, path);
2248
+ }
2249
+ getResource(address) {
2250
+ const i = this.index.get(addressKey(address));
2251
+ return i !== void 0 ? this.state.resources[i] : void 0;
2252
+ }
2253
+ setResource(resource) {
2254
+ const key = addressKey(resource.address);
2255
+ const i = this.index.get(key);
2256
+ if (i !== void 0) {
2257
+ this.state.resources[i] = resource;
2258
+ } else {
2259
+ this.index.set(key, this.state.resources.length);
2260
+ this.state.resources.push(resource);
2261
+ }
2262
+ }
2263
+ removeResource(address) {
2264
+ const key = addressKey(address);
2265
+ const i = this.index.get(key);
2266
+ if (i === void 0) return;
2267
+ this.state.resources.splice(i, 1);
2268
+ this.index.delete(key);
2269
+ for (let j = i; j < this.state.resources.length; j++) {
2270
+ this.index.set(addressKey(this.state.resources[j].address), j);
2271
+ }
2272
+ }
2273
+ listResources() {
2274
+ return [...this.state.resources];
2275
+ }
2276
+ findResource(query) {
2277
+ return this.state.resources.find((resource) => {
2278
+ const matchType = resource.address.type === query.type;
2279
+ const matchName = resource.address.name === query.name;
2280
+ const matchProvider = !query.provider || resource.address.provider === query.provider;
2281
+ return matchType && matchName && matchProvider;
2282
+ });
2283
+ }
2284
+ getStateFile() {
2285
+ return this.state;
2286
+ }
2287
+ async save() {
2288
+ await mkdir(dirname4(this.path), { recursive: true });
2289
+ const tmpPath = `${this.path}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
2290
+ try {
2291
+ await writeFile(tmpPath, `${JSON.stringify(this.state, null, 2)}
2292
+ `);
2293
+ await rename(tmpPath, this.path);
2294
+ } catch (error) {
2295
+ await unlink(tmpPath).catch(() => {
2296
+ });
2297
+ throw error;
2298
+ }
2299
+ }
2300
+ };
2301
+
2302
+ // src/internal/state/local-file-state-backend.ts
2303
+ import { resolve as resolve5 } from "path";
2304
+
2305
+ // src/internal/utils/paths.ts
2306
+ function deriveStatePath(configPath) {
2307
+ return /\.ya?ml$/i.test(configPath) ? configPath.replace(/\.ya?ml$/i, ".state.json") : `${configPath}.state.json`;
2308
+ }
2309
+
2310
+ // src/internal/state/backend.ts
2311
+ function cloneStateFile(state) {
2312
+ return structuredClone(state);
2313
+ }
2314
+
2315
+ // src/internal/state/in-memory-state-manager.ts
2316
+ var InMemoryStateManager = class _InMemoryStateManager {
2317
+ state;
2318
+ index;
2319
+ constructor(state) {
2320
+ this.state = state;
2321
+ this.index = this.buildIndex();
2322
+ }
2323
+ buildIndex() {
2324
+ const idx = /* @__PURE__ */ new Map();
2325
+ for (let i = 0; i < this.state.resources.length; i++) {
2326
+ idx.set(addressKey(this.state.resources[i].address), i);
2327
+ }
2328
+ return idx;
2329
+ }
2330
+ static fromJSON(data) {
2331
+ return new _InMemoryStateManager(structuredClone(data));
2332
+ }
2333
+ static empty() {
2334
+ return new _InMemoryStateManager({ resources: [] });
2335
+ }
2336
+ getResource(address) {
2337
+ const i = this.index.get(addressKey(address));
2338
+ return i !== void 0 ? this.state.resources[i] : void 0;
2339
+ }
2340
+ setResource(resource) {
2341
+ const key = addressKey(resource.address);
2342
+ const i = this.index.get(key);
2343
+ if (i !== void 0) {
2344
+ this.state.resources[i] = resource;
2345
+ } else {
2346
+ this.index.set(key, this.state.resources.length);
2347
+ this.state.resources.push(resource);
2348
+ }
2349
+ }
2350
+ removeResource(address) {
2351
+ const key = addressKey(address);
2352
+ const i = this.index.get(key);
2353
+ if (i === void 0) return;
2354
+ this.state.resources.splice(i, 1);
2355
+ this.index.delete(key);
2356
+ for (let j = i; j < this.state.resources.length; j++) {
2357
+ this.index.set(addressKey(this.state.resources[j].address), j);
2358
+ }
2359
+ }
2360
+ listResources() {
2361
+ return [...this.state.resources];
2362
+ }
2363
+ findResource(query) {
2364
+ return this.state.resources.find((resource) => {
2365
+ const matchType = resource.address.type === query.type;
2366
+ const matchName = resource.address.name === query.name;
2367
+ const matchProvider = !query.provider || resource.address.provider === query.provider;
2368
+ return matchType && matchName && matchProvider;
2369
+ });
2370
+ }
2371
+ getStateFile() {
2372
+ return this.state;
2373
+ }
2374
+ async save() {
2375
+ }
2376
+ };
2377
+
2378
+ // src/internal/state/local-file-state-backend.ts
2379
+ var LocalFileStateBackend = class {
2380
+ configPath;
2381
+ statePath;
2382
+ constructor(options = {}) {
2383
+ this.configPath = options.configPath ? resolve5(options.configPath) : void 0;
2384
+ this.statePath = options.statePath ? resolve5(options.statePath) : void 0;
2385
+ }
2386
+ async read(scope, fn) {
2387
+ const manager = await StateManager.load(this.getStatePath(scope));
2388
+ const readonlyState = InMemoryStateManager.fromJSON(cloneStateFile(manager.getStateFile()));
2389
+ return fn(readonlyState);
2390
+ }
2391
+ async write(scope, fn) {
2392
+ const manager = await StateManager.load(this.getStatePath(scope));
2393
+ const result = await fn(manager);
2394
+ await manager.save();
2395
+ return result;
2396
+ }
2397
+ getStatePath(_scope) {
2398
+ if (this.statePath) return this.statePath;
2399
+ if (this.configPath) return deriveStatePath(this.configPath);
2400
+ throw new Error("LocalFileStateBackend requires configPath or statePath");
2401
+ }
2402
+ };
2403
+
2404
+ export {
2405
+ createProjectRuntime,
2406
+ readProjectRuntime,
2407
+ writeProjectRuntime,
2408
+ getRuntimeProvider,
2409
+ contentHash,
2410
+ computeResourceHash,
2411
+ emitRuntimeFeedback,
2412
+ addressKey,
2413
+ requireRef,
2414
+ resolveDeploymentRefs,
2415
+ inspectSkillSource,
2416
+ collectDependencyClosure,
2417
+ buildDependencyGraph,
2418
+ planProjectWithStateBackend,
2419
+ syncProjectResourcesWithStateBackend,
2420
+ importResource,
2421
+ planProjectContext,
2422
+ executePlannedProject,
2423
+ selectDestructive,
2424
+ decideDestructive,
2425
+ cloneStateFile,
2426
+ InMemoryStateManager,
2427
+ StateManager,
2428
+ LocalFileStateBackend
2429
+ };