agentplane 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,12 @@ export type UpgradeFlags = {
9
9
  backup: boolean;
10
10
  yes: boolean;
11
11
  };
12
+ export declare function normalizeFrameworkSourceForUpgrade(source: string): {
13
+ source: string;
14
+ owner: string;
15
+ repo: string;
16
+ migrated: boolean;
17
+ };
12
18
  export declare function cmdUpgradeParsed(opts: {
13
19
  cwd: string;
14
20
  rootOverride?: string;
@@ -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;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;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,CAuQlB"}
@@ -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,18 @@ 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
+ }
33
45
  async function resolveUpgradeRoot(extractedDir) {
34
46
  const entries = await readdir(extractedDir, { withFileTypes: true });
35
47
  const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -58,6 +70,193 @@ function isAllowedUpgradePath(relPath) {
58
70
  return true;
59
71
  return relPath.startsWith(".agentplane/");
60
72
  }
73
+ const LOCAL_OVERRIDES_START = "<!-- AGENTPLANE:LOCAL-START -->";
74
+ const LOCAL_OVERRIDES_END = "<!-- AGENTPLANE:LOCAL-END -->";
75
+ function extractLocalOverridesBlock(text) {
76
+ const start = text.indexOf(LOCAL_OVERRIDES_START);
77
+ const end = text.indexOf(LOCAL_OVERRIDES_END);
78
+ if (start === -1 || end === -1 || end < start)
79
+ return null;
80
+ return text.slice(start + LOCAL_OVERRIDES_START.length, end).trim();
81
+ }
82
+ function withLocalOverridesBlock(base, localOverrides) {
83
+ const start = base.indexOf(LOCAL_OVERRIDES_START);
84
+ const end = base.indexOf(LOCAL_OVERRIDES_END);
85
+ if (start === -1 || end === -1 || end < start) {
86
+ const suffix = "\n\n## Local Overrides (preserved across upgrades)\n\n" +
87
+ `${LOCAL_OVERRIDES_START}\n` +
88
+ (localOverrides.trim() ? `${localOverrides.trim()}\n` : "") +
89
+ `${LOCAL_OVERRIDES_END}\n`;
90
+ return `${base.trimEnd()}${suffix}`;
91
+ }
92
+ const before = base.slice(0, start + LOCAL_OVERRIDES_START.length);
93
+ const after = base.slice(end);
94
+ return `${before}\n${localOverrides.trim() ? `${localOverrides.trim()}\n` : ""}${after}`;
95
+ }
96
+ function parseH2Sections(text) {
97
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
98
+ const sections = new Map();
99
+ let current = null;
100
+ let buf = [];
101
+ const flush = () => {
102
+ if (!current)
103
+ return;
104
+ if (!sections.has(current)) {
105
+ sections.set(current, buf.join("\n").trimEnd());
106
+ }
107
+ };
108
+ for (const line of lines) {
109
+ const m = /^##\s+(.+?)\s*$/.exec(line);
110
+ if (m) {
111
+ flush();
112
+ current = (m[1] ?? "").trim();
113
+ buf = [];
114
+ continue;
115
+ }
116
+ if (current)
117
+ buf.push(line);
118
+ }
119
+ flush();
120
+ return sections;
121
+ }
122
+ function mergeAgentsPolicyMarkdown(incoming, current) {
123
+ const local = extractLocalOverridesBlock(current);
124
+ if (local !== null) {
125
+ return withLocalOverridesBlock(incoming, local);
126
+ }
127
+ // Fallback: if the user edited AGENTS.md without the local markers, preserve their changes by
128
+ // appending differing/extra sections into a dedicated local overrides block.
129
+ const incomingSections = parseH2Sections(incoming);
130
+ const currentSections = parseH2Sections(current);
131
+ const overrides = [];
132
+ for (const [title, body] of currentSections.entries()) {
133
+ const incomingBody = incomingSections.get(title);
134
+ if (incomingBody === undefined) {
135
+ overrides.push(`### Added section: ${title}\n\n${body.trim()}\n`);
136
+ continue;
137
+ }
138
+ if (incomingBody.trim() !== body.trim()) {
139
+ overrides.push(`### Local edits for: ${title}\n\n${body.trim()}\n`);
140
+ }
141
+ }
142
+ if (overrides.length === 0)
143
+ return incoming;
144
+ return withLocalOverridesBlock(incoming, overrides.join("\n"));
145
+ }
146
+ function isJsonRecord(value) {
147
+ return !!value && typeof value === "object" && !Array.isArray(value);
148
+ }
149
+ function mergeAgentJson(incomingText, currentText) {
150
+ let incoming;
151
+ let current;
152
+ try {
153
+ incoming = JSON.parse(incomingText);
154
+ current = JSON.parse(currentText);
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ if (!isJsonRecord(incoming) || !isJsonRecord(current))
160
+ return null;
161
+ const out = { ...incoming };
162
+ for (const [k, curVal] of Object.entries(current)) {
163
+ const incVal = incoming[k];
164
+ if (incVal === undefined) {
165
+ out[k] = curVal;
166
+ continue;
167
+ }
168
+ if (Array.isArray(incVal) && Array.isArray(curVal)) {
169
+ const merged = [...incVal];
170
+ for (const item of curVal) {
171
+ if (!merged.some((x) => JSON.stringify(x) === JSON.stringify(item)))
172
+ merged.push(item);
173
+ }
174
+ out[k] = merged;
175
+ continue;
176
+ }
177
+ if (isJsonRecord(incVal) && isJsonRecord(curVal)) {
178
+ out[k] = { ...incVal, ...curVal };
179
+ continue;
180
+ }
181
+ if (curVal !== incVal && curVal !== null && curVal !== "") {
182
+ out[k] = curVal;
183
+ continue;
184
+ }
185
+ out[k] = incVal;
186
+ }
187
+ return JSON.stringify(out, null, 2) + "\n";
188
+ }
189
+ function mergeAgentJson3Way(opts) {
190
+ let incoming;
191
+ let current;
192
+ let base;
193
+ try {
194
+ incoming = JSON.parse(opts.incomingText);
195
+ current = JSON.parse(opts.currentText);
196
+ base = JSON.parse(opts.baseText);
197
+ }
198
+ catch {
199
+ return null;
200
+ }
201
+ if (!isJsonRecord(incoming) || !isJsonRecord(current) || !isJsonRecord(base))
202
+ return null;
203
+ const keys = new Set([...Object.keys(incoming), ...Object.keys(current), ...Object.keys(base)]);
204
+ const out = {};
205
+ for (const key of keys) {
206
+ const incVal = incoming[key];
207
+ const curVal = current[key];
208
+ const baseVal = base[key];
209
+ // Arrays: always take incoming as base; if user changed vs base, append user-only items.
210
+ if (Array.isArray(incVal) && Array.isArray(curVal) && Array.isArray(baseVal)) {
211
+ const merged = [...incVal];
212
+ const userChanged = JSON.stringify(curVal) !== JSON.stringify(baseVal);
213
+ if (userChanged) {
214
+ for (const item of curVal) {
215
+ if (!merged.some((x) => JSON.stringify(x) === JSON.stringify(item)))
216
+ merged.push(item);
217
+ }
218
+ }
219
+ out[key] = merged;
220
+ continue;
221
+ }
222
+ // Objects: shallow merge; for each subkey, prefer incoming unless user changed vs base.
223
+ if (isJsonRecord(incVal) && isJsonRecord(curVal) && isJsonRecord(baseVal)) {
224
+ const merged = { ...incVal };
225
+ const subKeys = new Set([
226
+ ...Object.keys(incVal),
227
+ ...Object.keys(curVal),
228
+ ...Object.keys(baseVal),
229
+ ]);
230
+ for (const sk of subKeys) {
231
+ const incSub = incVal[sk];
232
+ const curSub = curVal[sk];
233
+ const baseSub = baseVal[sk];
234
+ const userChanged = JSON.stringify(curSub) !== JSON.stringify(baseSub);
235
+ if (userChanged)
236
+ merged[sk] = curSub;
237
+ else if (incSub !== undefined)
238
+ merged[sk] = incSub;
239
+ else if (curSub !== undefined)
240
+ merged[sk] = curSub;
241
+ }
242
+ out[key] = merged;
243
+ continue;
244
+ }
245
+ // Scalars: prefer incoming unless the user changed vs base.
246
+ if (JSON.stringify(curVal) !== JSON.stringify(baseVal)) {
247
+ if (curVal !== undefined)
248
+ out[key] = curVal;
249
+ else if (incVal !== undefined)
250
+ out[key] = incVal;
251
+ continue;
252
+ }
253
+ if (incVal !== undefined)
254
+ out[key] = incVal;
255
+ else if (curVal !== undefined)
256
+ out[key] = curVal;
257
+ }
258
+ return JSON.stringify(out, null, 2) + "\n";
259
+ }
61
260
  export async function cmdUpgradeParsed(opts) {
62
261
  const flags = opts.flags;
63
262
  if ((flags.bundle && !flags.checksum) || (!flags.bundle && flags.checksum)) {
@@ -74,7 +273,13 @@ export async function cmdUpgradeParsed(opts) {
74
273
  rootOverride: opts.rootOverride ?? null,
75
274
  });
76
275
  const loaded = await loadConfig(resolved.agentplaneDir);
77
- const source = flags.source ?? loaded.config.framework.source;
276
+ const sourceFromFlags = typeof flags.source === "string" && flags.source.trim().length > 0;
277
+ const originalSource = flags.source ?? loaded.config.framework.source;
278
+ const normalized = normalizeFrameworkSourceForUpgrade(originalSource);
279
+ const source = normalized.source;
280
+ if (normalized.migrated) {
281
+ process.stderr.write(`${warnMessage(`config.framework.source uses deprecated repo basilisk-labs/agent-plane; using ${source}`)}\n`);
282
+ }
78
283
  let networkApproved = false;
79
284
  const ensureApproved = async (reason) => {
80
285
  if (networkApproved)
@@ -106,7 +311,7 @@ export async function cmdUpgradeParsed(opts) {
106
311
  }
107
312
  }
108
313
  else {
109
- const { owner, repo } = parseGitHubRepo(source);
314
+ const { owner, repo } = normalized;
110
315
  const releaseUrl = flags.tag
111
316
  ? `https://api.github.com/repos/${owner}/${repo}/releases/tags/${flags.tag}`
112
317
  : `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
@@ -156,6 +361,15 @@ export async function cmdUpgradeParsed(opts) {
156
361
  const updates = [];
157
362
  const skipped = [];
158
363
  const fileContents = new Map();
364
+ const merged = [];
365
+ const baselineDir = path.join(resolved.agentplaneDir, "upgrade", "baseline");
366
+ const toBaselineKey = (rel) => {
367
+ if (rel === "AGENTS.md")
368
+ return "AGENTS.md";
369
+ if (rel.startsWith(".agentplane/"))
370
+ return rel.slice(".agentplane/".length);
371
+ return null;
372
+ };
159
373
  for (const filePath of files) {
160
374
  let rel = path.relative(bundleRoot, filePath).replaceAll("\\", "/");
161
375
  if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
@@ -188,14 +402,49 @@ export async function cmdUpgradeParsed(opts) {
188
402
  message: `Upgrade target is a directory: ${rel}`,
189
403
  });
190
404
  }
191
- const data = await readFile(filePath);
405
+ if (rel === ".agentplane/config.json") {
406
+ // Never overwrite local config during upgrade.
407
+ skipped.push(rel);
408
+ continue;
409
+ }
410
+ let data = await readFile(filePath);
411
+ if (kind !== null) {
412
+ const existing = await readFile(destPath, "utf8");
413
+ if (rel === "AGENTS.md") {
414
+ const mergedText = mergeAgentsPolicyMarkdown(data.toString("utf8"), existing);
415
+ data = Buffer.from(mergedText, "utf8");
416
+ merged.push(rel);
417
+ }
418
+ else if (rel.startsWith(".agentplane/agents/") && rel.endsWith(".json")) {
419
+ const baselineKey = toBaselineKey(rel);
420
+ let mergedText = null;
421
+ if (baselineKey) {
422
+ try {
423
+ const baselineText = await readFile(path.join(baselineDir, baselineKey), "utf8");
424
+ mergedText = mergeAgentJson3Way({
425
+ incomingText: data.toString("utf8"),
426
+ currentText: existing,
427
+ baseText: baselineText,
428
+ });
429
+ }
430
+ catch {
431
+ mergedText = null;
432
+ }
433
+ }
434
+ mergedText ??= mergeAgentJson(data.toString("utf8"), existing);
435
+ if (mergedText) {
436
+ data = Buffer.from(mergedText, "utf8");
437
+ merged.push(rel);
438
+ }
439
+ }
440
+ }
192
441
  fileContents.set(rel, data);
193
442
  if (kind === null) {
194
443
  additions.push(rel);
195
444
  }
196
445
  else {
197
- const existing = await readFile(destPath);
198
- if (Buffer.compare(existing, data) === 0) {
446
+ const existingBuf = await readFile(destPath);
447
+ if (Buffer.compare(existingBuf, data) === 0) {
199
448
  skipped.push(rel);
200
449
  }
201
450
  else {
@@ -211,8 +460,13 @@ export async function cmdUpgradeParsed(opts) {
211
460
  process.stdout.write(`UPDATE ${rel}\n`);
212
461
  for (const rel of skipped)
213
462
  process.stdout.write(`SKIP ${rel}\n`);
463
+ for (const rel of merged)
464
+ process.stdout.write(`MERGE ${rel}\n`);
214
465
  return 0;
215
466
  }
467
+ if (skipped.includes(".agentplane/config.json")) {
468
+ process.stderr.write(`${warnMessage("upgrade bundle includes .agentplane/config.json; skipping to preserve local configuration")}\n`);
469
+ }
216
470
  for (const rel of [...additions, ...updates]) {
217
471
  const destPath = path.join(resolved.gitRoot, rel);
218
472
  if (flags.backup && (await fileExists(destPath))) {
@@ -222,8 +476,18 @@ export async function cmdUpgradeParsed(opts) {
222
476
  const data = fileContents.get(rel);
223
477
  if (data)
224
478
  await writeFile(destPath, data);
479
+ // Record a baseline copy for future three-way merges.
480
+ const baselineKey = toBaselineKey(rel);
481
+ if (baselineKey && data) {
482
+ const baselinePath = path.join(baselineDir, baselineKey);
483
+ await mkdir(path.dirname(baselinePath), { recursive: true });
484
+ await writeFile(baselinePath, data);
485
+ }
225
486
  }
226
487
  const raw = { ...loaded.raw };
488
+ if (!sourceFromFlags && normalized.migrated) {
489
+ setByDottedKey(raw, "framework.source", source);
490
+ }
227
491
  setByDottedKey(raw, "framework.last_update", new Date().toISOString());
228
492
  await saveConfig(resolved.agentplaneDir, raw);
229
493
  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.1",
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.1",
58
58
  "yauzl": "^2.10.0"
59
59
  },
60
60
  "devDependencies": {