agentplane 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,9 +9,37 @@ export type UpgradeFlags = {
9
9
  backup: boolean;
10
10
  yes: boolean;
11
11
  };
12
+ type GitHubRelease = {
13
+ assets?: {
14
+ name?: string;
15
+ browser_download_url?: string;
16
+ }[];
17
+ tarball_url?: string;
18
+ };
19
+ export declare function normalizeFrameworkSourceForUpgrade(source: string): {
20
+ source: string;
21
+ owner: string;
22
+ repo: string;
23
+ migrated: boolean;
24
+ };
25
+ export declare function resolveUpgradeDownloadFromRelease(opts: {
26
+ release: GitHubRelease;
27
+ owner: string;
28
+ repo: string;
29
+ assetName: string;
30
+ checksumName: string;
31
+ }): {
32
+ kind: "assets";
33
+ bundleUrl: string;
34
+ checksumUrl: string;
35
+ } | {
36
+ kind: "tarball";
37
+ tarballUrl: string;
38
+ };
12
39
  export declare function cmdUpgradeParsed(opts: {
13
40
  cwd: string;
14
41
  rootOverride?: string;
15
42
  flags: UpgradeFlags;
16
43
  }): Promise<number>;
44
+ export {};
17
45
  //# sourceMappingURL=upgrade.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/commands/upgrade.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAgDF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6LlB"}
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/commands/upgrade.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAmBF,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,MAAM,GAAG;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB,CAWA;AAED,wBAAgB,iCAAiC,CAAC,IAAI,EAAE;IACtD,OAAO,EAAE,aAAa,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,GACG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAqB1C;AAkOD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgSlB"}
@@ -6,7 +6,7 @@ import { backupPath, fileExists, getPathKind } from "../cli/fs-utils.js";
6
6
  import { downloadToFile, fetchJson } from "../cli/http.js";
7
7
  import { parseSha256Text, sha256File } from "../cli/checksum.js";
8
8
  import { extractArchive } from "../cli/archive.js";
9
- import { invalidFieldMessage, invalidValueMessage, requiredFieldMessage } from "../cli/output.js";
9
+ import { invalidFieldMessage, invalidValueMessage, requiredFieldMessage, warnMessage, } from "../cli/output.js";
10
10
  import { exitCodeForError } from "../cli/exit-codes.js";
11
11
  import { CliError } from "../shared/errors.js";
12
12
  import { ensureNetworkApproved } from "./shared/network-approval.js";
@@ -30,6 +30,39 @@ function parseGitHubRepo(source) {
30
30
  throw new Error(invalidValueMessage("GitHub repo URL", trimmed, "owner/repo"));
31
31
  }
32
32
  }
33
+ export function normalizeFrameworkSourceForUpgrade(source) {
34
+ const { owner, repo } = parseGitHubRepo(source);
35
+ if (owner === "basilisk-labs" && repo === "agent-plane") {
36
+ return {
37
+ source: `https://github.com/${owner}/agentplane`,
38
+ owner,
39
+ repo: "agentplane",
40
+ migrated: true,
41
+ };
42
+ }
43
+ return { source: `https://github.com/${owner}/${repo}`, owner, repo, migrated: false };
44
+ }
45
+ export function resolveUpgradeDownloadFromRelease(opts) {
46
+ const assets = Array.isArray(opts.release.assets) ? opts.release.assets : [];
47
+ const asset = assets.find((a) => a?.name === opts.assetName);
48
+ const checksumAsset = assets.find((a) => a?.name === opts.checksumName);
49
+ if (asset?.browser_download_url && checksumAsset?.browser_download_url) {
50
+ return {
51
+ kind: "assets",
52
+ bundleUrl: asset.browser_download_url,
53
+ checksumUrl: checksumAsset.browser_download_url,
54
+ };
55
+ }
56
+ const tarballUrl = typeof opts.release.tarball_url === "string" ? opts.release.tarball_url : "";
57
+ if (!tarballUrl) {
58
+ throw new CliError({
59
+ exitCode: exitCodeForError("E_NETWORK"),
60
+ code: "E_NETWORK",
61
+ message: `Upgrade assets not found in ${opts.owner}/${opts.repo} release`,
62
+ });
63
+ }
64
+ return { kind: "tarball", tarballUrl };
65
+ }
33
66
  async function resolveUpgradeRoot(extractedDir) {
34
67
  const entries = await readdir(extractedDir, { withFileTypes: true });
35
68
  const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -58,6 +91,193 @@ function isAllowedUpgradePath(relPath) {
58
91
  return true;
59
92
  return relPath.startsWith(".agentplane/");
60
93
  }
94
+ const LOCAL_OVERRIDES_START = "<!-- AGENTPLANE:LOCAL-START -->";
95
+ const LOCAL_OVERRIDES_END = "<!-- AGENTPLANE:LOCAL-END -->";
96
+ function extractLocalOverridesBlock(text) {
97
+ const start = text.indexOf(LOCAL_OVERRIDES_START);
98
+ const end = text.indexOf(LOCAL_OVERRIDES_END);
99
+ if (start === -1 || end === -1 || end < start)
100
+ return null;
101
+ return text.slice(start + LOCAL_OVERRIDES_START.length, end).trim();
102
+ }
103
+ function withLocalOverridesBlock(base, localOverrides) {
104
+ const start = base.indexOf(LOCAL_OVERRIDES_START);
105
+ const end = base.indexOf(LOCAL_OVERRIDES_END);
106
+ if (start === -1 || end === -1 || end < start) {
107
+ const suffix = "\n\n## Local Overrides (preserved across upgrades)\n\n" +
108
+ `${LOCAL_OVERRIDES_START}\n` +
109
+ (localOverrides.trim() ? `${localOverrides.trim()}\n` : "") +
110
+ `${LOCAL_OVERRIDES_END}\n`;
111
+ return `${base.trimEnd()}${suffix}`;
112
+ }
113
+ const before = base.slice(0, start + LOCAL_OVERRIDES_START.length);
114
+ const after = base.slice(end);
115
+ return `${before}\n${localOverrides.trim() ? `${localOverrides.trim()}\n` : ""}${after}`;
116
+ }
117
+ function parseH2Sections(text) {
118
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
119
+ const sections = new Map();
120
+ let current = null;
121
+ let buf = [];
122
+ const flush = () => {
123
+ if (!current)
124
+ return;
125
+ if (!sections.has(current)) {
126
+ sections.set(current, buf.join("\n").trimEnd());
127
+ }
128
+ };
129
+ for (const line of lines) {
130
+ const m = /^##\s+(.+?)\s*$/.exec(line);
131
+ if (m) {
132
+ flush();
133
+ current = (m[1] ?? "").trim();
134
+ buf = [];
135
+ continue;
136
+ }
137
+ if (current)
138
+ buf.push(line);
139
+ }
140
+ flush();
141
+ return sections;
142
+ }
143
+ function mergeAgentsPolicyMarkdown(incoming, current) {
144
+ const local = extractLocalOverridesBlock(current);
145
+ if (local !== null) {
146
+ return withLocalOverridesBlock(incoming, local);
147
+ }
148
+ // Fallback: if the user edited AGENTS.md without the local markers, preserve their changes by
149
+ // appending differing/extra sections into a dedicated local overrides block.
150
+ const incomingSections = parseH2Sections(incoming);
151
+ const currentSections = parseH2Sections(current);
152
+ const overrides = [];
153
+ for (const [title, body] of currentSections.entries()) {
154
+ const incomingBody = incomingSections.get(title);
155
+ if (incomingBody === undefined) {
156
+ overrides.push(`### Added section: ${title}\n\n${body.trim()}\n`);
157
+ continue;
158
+ }
159
+ if (incomingBody.trim() !== body.trim()) {
160
+ overrides.push(`### Local edits for: ${title}\n\n${body.trim()}\n`);
161
+ }
162
+ }
163
+ if (overrides.length === 0)
164
+ return incoming;
165
+ return withLocalOverridesBlock(incoming, overrides.join("\n"));
166
+ }
167
+ function isJsonRecord(value) {
168
+ return !!value && typeof value === "object" && !Array.isArray(value);
169
+ }
170
+ function mergeAgentJson(incomingText, currentText) {
171
+ let incoming;
172
+ let current;
173
+ try {
174
+ incoming = JSON.parse(incomingText);
175
+ current = JSON.parse(currentText);
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ if (!isJsonRecord(incoming) || !isJsonRecord(current))
181
+ return null;
182
+ const out = { ...incoming };
183
+ for (const [k, curVal] of Object.entries(current)) {
184
+ const incVal = incoming[k];
185
+ if (incVal === undefined) {
186
+ out[k] = curVal;
187
+ continue;
188
+ }
189
+ if (Array.isArray(incVal) && Array.isArray(curVal)) {
190
+ const merged = [...incVal];
191
+ for (const item of curVal) {
192
+ if (!merged.some((x) => JSON.stringify(x) === JSON.stringify(item)))
193
+ merged.push(item);
194
+ }
195
+ out[k] = merged;
196
+ continue;
197
+ }
198
+ if (isJsonRecord(incVal) && isJsonRecord(curVal)) {
199
+ out[k] = { ...incVal, ...curVal };
200
+ continue;
201
+ }
202
+ if (curVal !== incVal && curVal !== null && curVal !== "") {
203
+ out[k] = curVal;
204
+ continue;
205
+ }
206
+ out[k] = incVal;
207
+ }
208
+ return JSON.stringify(out, null, 2) + "\n";
209
+ }
210
+ function mergeAgentJson3Way(opts) {
211
+ let incoming;
212
+ let current;
213
+ let base;
214
+ try {
215
+ incoming = JSON.parse(opts.incomingText);
216
+ current = JSON.parse(opts.currentText);
217
+ base = JSON.parse(opts.baseText);
218
+ }
219
+ catch {
220
+ return null;
221
+ }
222
+ if (!isJsonRecord(incoming) || !isJsonRecord(current) || !isJsonRecord(base))
223
+ return null;
224
+ const keys = new Set([...Object.keys(incoming), ...Object.keys(current), ...Object.keys(base)]);
225
+ const out = {};
226
+ for (const key of keys) {
227
+ const incVal = incoming[key];
228
+ const curVal = current[key];
229
+ const baseVal = base[key];
230
+ // Arrays: always take incoming as base; if user changed vs base, append user-only items.
231
+ if (Array.isArray(incVal) && Array.isArray(curVal) && Array.isArray(baseVal)) {
232
+ const merged = [...incVal];
233
+ const userChanged = JSON.stringify(curVal) !== JSON.stringify(baseVal);
234
+ if (userChanged) {
235
+ for (const item of curVal) {
236
+ if (!merged.some((x) => JSON.stringify(x) === JSON.stringify(item)))
237
+ merged.push(item);
238
+ }
239
+ }
240
+ out[key] = merged;
241
+ continue;
242
+ }
243
+ // Objects: shallow merge; for each subkey, prefer incoming unless user changed vs base.
244
+ if (isJsonRecord(incVal) && isJsonRecord(curVal) && isJsonRecord(baseVal)) {
245
+ const merged = { ...incVal };
246
+ const subKeys = new Set([
247
+ ...Object.keys(incVal),
248
+ ...Object.keys(curVal),
249
+ ...Object.keys(baseVal),
250
+ ]);
251
+ for (const sk of subKeys) {
252
+ const incSub = incVal[sk];
253
+ const curSub = curVal[sk];
254
+ const baseSub = baseVal[sk];
255
+ const userChanged = JSON.stringify(curSub) !== JSON.stringify(baseSub);
256
+ if (userChanged)
257
+ merged[sk] = curSub;
258
+ else if (incSub !== undefined)
259
+ merged[sk] = incSub;
260
+ else if (curSub !== undefined)
261
+ merged[sk] = curSub;
262
+ }
263
+ out[key] = merged;
264
+ continue;
265
+ }
266
+ // Scalars: prefer incoming unless the user changed vs base.
267
+ if (JSON.stringify(curVal) !== JSON.stringify(baseVal)) {
268
+ if (curVal !== undefined)
269
+ out[key] = curVal;
270
+ else if (incVal !== undefined)
271
+ out[key] = incVal;
272
+ continue;
273
+ }
274
+ if (incVal !== undefined)
275
+ out[key] = incVal;
276
+ else if (curVal !== undefined)
277
+ out[key] = curVal;
278
+ }
279
+ return JSON.stringify(out, null, 2) + "\n";
280
+ }
61
281
  export async function cmdUpgradeParsed(opts) {
62
282
  const flags = opts.flags;
63
283
  if ((flags.bundle && !flags.checksum) || (!flags.bundle && flags.checksum)) {
@@ -74,7 +294,13 @@ export async function cmdUpgradeParsed(opts) {
74
294
  rootOverride: opts.rootOverride ?? null,
75
295
  });
76
296
  const loaded = await loadConfig(resolved.agentplaneDir);
77
- const source = flags.source ?? loaded.config.framework.source;
297
+ const sourceFromFlags = typeof flags.source === "string" && flags.source.trim().length > 0;
298
+ const originalSource = flags.source ?? loaded.config.framework.source;
299
+ const normalized = normalizeFrameworkSourceForUpgrade(originalSource);
300
+ const source = normalized.source;
301
+ if (normalized.migrated) {
302
+ process.stderr.write(`${warnMessage(`config.framework.source uses deprecated repo basilisk-labs/agent-plane; using ${source}`)}\n`);
303
+ }
78
304
  let networkApproved = false;
79
305
  const ensureApproved = async (reason) => {
80
306
  if (networkApproved)
@@ -88,6 +314,9 @@ export async function cmdUpgradeParsed(opts) {
88
314
  tempRoot = await mkdtemp(path.join(os.tmpdir(), "agentplane-upgrade-"));
89
315
  let bundlePath = "";
90
316
  let checksumPath = "";
317
+ // GitHub release tarballs contain the full repository. When we fall back to tarball_url,
318
+ // we must ignore non-upgrade paths instead of failing validation.
319
+ let allowNonUpgradePaths = false;
91
320
  if (flags.bundle) {
92
321
  const isUrl = flags.bundle.startsWith("http://") || flags.bundle.startsWith("https://");
93
322
  bundlePath = isUrl ? path.join(tempRoot, "bundle.tar.gz") : path.resolve(flags.bundle);
@@ -106,44 +335,52 @@ export async function cmdUpgradeParsed(opts) {
106
335
  }
107
336
  }
108
337
  else {
109
- const { owner, repo } = parseGitHubRepo(source);
338
+ const { owner, repo } = normalized;
110
339
  const releaseUrl = flags.tag
111
340
  ? `https://api.github.com/repos/${owner}/${repo}/releases/tags/${flags.tag}`
112
341
  : `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
113
342
  await ensureApproved("upgrade fetches release metadata and downloads assets from the network");
114
- const release = (await fetchJson(releaseUrl));
115
- const assets = Array.isArray(release.assets) ? release.assets : [];
116
343
  const assetName = flags.asset ?? DEFAULT_UPGRADE_ASSET;
117
344
  const checksumName = flags.checksumAsset ?? DEFAULT_UPGRADE_CHECKSUM_ASSET;
118
- const asset = assets.find((entry) => entry.name === assetName);
119
- const checksumAsset = assets.find((entry) => entry.name === checksumName);
120
- if (!asset?.browser_download_url || !checksumAsset?.browser_download_url) {
345
+ const release = (await fetchJson(releaseUrl));
346
+ const download = resolveUpgradeDownloadFromRelease({
347
+ release,
348
+ owner,
349
+ repo,
350
+ assetName,
351
+ checksumName,
352
+ });
353
+ if (download.kind === "assets") {
354
+ bundlePath = path.join(tempRoot, assetName);
355
+ checksumPath = path.join(tempRoot, checksumName);
356
+ await downloadToFile(download.bundleUrl, bundlePath);
357
+ await downloadToFile(download.checksumUrl, checksumPath);
358
+ }
359
+ else {
360
+ process.stderr.write(`${warnMessage(`upgrade release does not include ${assetName}/${checksumName}; falling back to tarball_url without checksum verification`)}\n`);
361
+ allowNonUpgradePaths = true;
362
+ bundlePath = path.join(tempRoot, "source.tar.gz");
363
+ await downloadToFile(download.tarballUrl, bundlePath);
364
+ checksumPath = "";
365
+ }
366
+ }
367
+ if (checksumPath) {
368
+ const expected = parseSha256Text(await readFile(checksumPath, "utf8"));
369
+ if (!expected) {
121
370
  throw new CliError({
122
- exitCode: exitCodeForError("E_NETWORK"),
123
- code: "E_NETWORK",
124
- message: `Upgrade assets not found in ${owner}/${repo} release`,
371
+ exitCode: 3,
372
+ code: "E_VALIDATION",
373
+ message: "Upgrade checksum file is empty or invalid",
374
+ });
375
+ }
376
+ const actual = await sha256File(bundlePath);
377
+ if (actual !== expected) {
378
+ throw new CliError({
379
+ exitCode: 3,
380
+ code: "E_VALIDATION",
381
+ message: `Upgrade checksum mismatch (expected ${expected}, got ${actual})`,
125
382
  });
126
383
  }
127
- bundlePath = path.join(tempRoot, assetName);
128
- checksumPath = path.join(tempRoot, checksumName);
129
- await downloadToFile(asset.browser_download_url, bundlePath);
130
- await downloadToFile(checksumAsset.browser_download_url, checksumPath);
131
- }
132
- const expected = parseSha256Text(await readFile(checksumPath, "utf8"));
133
- if (!expected) {
134
- throw new CliError({
135
- exitCode: 3,
136
- code: "E_VALIDATION",
137
- message: "Upgrade checksum file is empty or invalid",
138
- });
139
- }
140
- const actual = await sha256File(bundlePath);
141
- if (actual !== expected) {
142
- throw new CliError({
143
- exitCode: 3,
144
- code: "E_VALIDATION",
145
- message: `Upgrade checksum mismatch (expected ${expected}, got ${actual})`,
146
- });
147
384
  }
148
385
  extractRoot = await mkdtemp(path.join(os.tmpdir(), "agentplane-upgrade-extract-"));
149
386
  await extractArchive({
@@ -156,6 +393,15 @@ export async function cmdUpgradeParsed(opts) {
156
393
  const updates = [];
157
394
  const skipped = [];
158
395
  const fileContents = new Map();
396
+ const merged = [];
397
+ const baselineDir = path.join(resolved.agentplaneDir, "upgrade", "baseline");
398
+ const toBaselineKey = (rel) => {
399
+ if (rel === "AGENTS.md")
400
+ return "AGENTS.md";
401
+ if (rel.startsWith(".agentplane/"))
402
+ return rel.slice(".agentplane/".length);
403
+ return null;
404
+ };
159
405
  for (const filePath of files) {
160
406
  let rel = path.relative(bundleRoot, filePath).replaceAll("\\", "/");
161
407
  if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
@@ -173,6 +419,9 @@ export async function cmdUpgradeParsed(opts) {
173
419
  });
174
420
  }
175
421
  if (!isAllowedUpgradePath(rel)) {
422
+ if (allowNonUpgradePaths) {
423
+ continue;
424
+ }
176
425
  throw new CliError({
177
426
  exitCode: 3,
178
427
  code: "E_VALIDATION",
@@ -188,14 +437,49 @@ export async function cmdUpgradeParsed(opts) {
188
437
  message: `Upgrade target is a directory: ${rel}`,
189
438
  });
190
439
  }
191
- const data = await readFile(filePath);
440
+ if (rel === ".agentplane/config.json") {
441
+ // Never overwrite local config during upgrade.
442
+ skipped.push(rel);
443
+ continue;
444
+ }
445
+ let data = await readFile(filePath);
446
+ if (kind !== null) {
447
+ const existing = await readFile(destPath, "utf8");
448
+ if (rel === "AGENTS.md") {
449
+ const mergedText = mergeAgentsPolicyMarkdown(data.toString("utf8"), existing);
450
+ data = Buffer.from(mergedText, "utf8");
451
+ merged.push(rel);
452
+ }
453
+ else if (rel.startsWith(".agentplane/agents/") && rel.endsWith(".json")) {
454
+ const baselineKey = toBaselineKey(rel);
455
+ let mergedText = null;
456
+ if (baselineKey) {
457
+ try {
458
+ const baselineText = await readFile(path.join(baselineDir, baselineKey), "utf8");
459
+ mergedText = mergeAgentJson3Way({
460
+ incomingText: data.toString("utf8"),
461
+ currentText: existing,
462
+ baseText: baselineText,
463
+ });
464
+ }
465
+ catch {
466
+ mergedText = null;
467
+ }
468
+ }
469
+ mergedText ??= mergeAgentJson(data.toString("utf8"), existing);
470
+ if (mergedText) {
471
+ data = Buffer.from(mergedText, "utf8");
472
+ merged.push(rel);
473
+ }
474
+ }
475
+ }
192
476
  fileContents.set(rel, data);
193
477
  if (kind === null) {
194
478
  additions.push(rel);
195
479
  }
196
480
  else {
197
- const existing = await readFile(destPath);
198
- if (Buffer.compare(existing, data) === 0) {
481
+ const existingBuf = await readFile(destPath);
482
+ if (Buffer.compare(existingBuf, data) === 0) {
199
483
  skipped.push(rel);
200
484
  }
201
485
  else {
@@ -203,6 +487,13 @@ export async function cmdUpgradeParsed(opts) {
203
487
  }
204
488
  }
205
489
  }
490
+ if (fileContents.size === 0) {
491
+ throw new CliError({
492
+ exitCode: 3,
493
+ code: "E_VALIDATION",
494
+ message: "Upgrade bundle contains no applicable files (expected AGENTS.md and/or .agentplane/).",
495
+ });
496
+ }
206
497
  if (flags.dryRun) {
207
498
  process.stdout.write(`Upgrade dry-run: ${additions.length} add, ${updates.length} update, ${skipped.length} unchanged\n`);
208
499
  for (const rel of additions)
@@ -211,8 +502,13 @@ export async function cmdUpgradeParsed(opts) {
211
502
  process.stdout.write(`UPDATE ${rel}\n`);
212
503
  for (const rel of skipped)
213
504
  process.stdout.write(`SKIP ${rel}\n`);
505
+ for (const rel of merged)
506
+ process.stdout.write(`MERGE ${rel}\n`);
214
507
  return 0;
215
508
  }
509
+ if (skipped.includes(".agentplane/config.json")) {
510
+ process.stderr.write(`${warnMessage("upgrade bundle includes .agentplane/config.json; skipping to preserve local configuration")}\n`);
511
+ }
216
512
  for (const rel of [...additions, ...updates]) {
217
513
  const destPath = path.join(resolved.gitRoot, rel);
218
514
  if (flags.backup && (await fileExists(destPath))) {
@@ -222,8 +518,18 @@ export async function cmdUpgradeParsed(opts) {
222
518
  const data = fileContents.get(rel);
223
519
  if (data)
224
520
  await writeFile(destPath, data);
521
+ // Record a baseline copy for future three-way merges.
522
+ const baselineKey = toBaselineKey(rel);
523
+ if (baselineKey && data) {
524
+ const baselinePath = path.join(baselineDir, baselineKey);
525
+ await mkdir(path.dirname(baselinePath), { recursive: true });
526
+ await writeFile(baselinePath, data);
527
+ }
225
528
  }
226
529
  const raw = { ...loaded.raw };
530
+ if (!sourceFromFlags && normalized.migrated) {
531
+ setByDottedKey(raw, "framework.source", source);
532
+ }
227
533
  setByDottedKey(raw, "framework.last_update", new Date().toISOString());
228
534
  await saveConfig(resolved.agentplaneDir, raw);
229
535
  process.stdout.write(`Upgrade applied: ${additions.length} add, ${updates.length} update, ${skipped.length} unchanged\n`);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "agentplane",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Agent Plane CLI for task workflows, recipes, and project automation.",
5
5
  "keywords": [
6
6
  "agentplane",
7
- "agent-plane",
7
+ "agentplane-framework",
8
8
  "cli",
9
9
  "tasks",
10
10
  "workflow",
@@ -54,7 +54,7 @@
54
54
  "prepublishOnly": "npm run prepack"
55
55
  },
56
56
  "dependencies": {
57
- "@agentplaneorg/core": "0.2.0",
57
+ "@agentplaneorg/core": "0.2.2",
58
58
  "yauzl": "^2.10.0"
59
59
  },
60
60
  "devDependencies": {