@anhlp/alp 0.0.0-beta.2 → 0.0.1-beta.3

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.
@@ -7,7 +7,7 @@ import { acquireDaemonLock, alpHome, assertPrivateDirectory, daemonPaths, lockAl
7
7
  import { daemonHeld, holdDaemon, releaseHold } from "@anhlp/alp-client/supervise";
8
8
  import { start, stop } from "./daemon.js";
9
9
  import { UsageError } from "./usage.js";
10
- var VERSION = true ? "0.0.0-beta.2" : "dev";
10
+ var VERSION = true ? "0.0.1-beta.3" : "dev";
11
11
  var BY = `alp ${VERSION}`;
12
12
  var size = (bytes) => bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${bytes} B`;
13
13
  var stamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.0-beta.2",
2
+ "version": "0.0.1-beta.3",
3
3
  "entries": {
4
4
  "packages/cli/dist/alpd.js": {
5
5
  "inputs": [
@@ -15,8 +15,12 @@ import { backupCommand, restoreCommand } from "./backup.js";
15
15
  var commands = { setup: (args2) => setup(args2, alpHome()), init: (args2) => project("init", args2), upgrade: (args2) => project("upgrade", args2), update: (args2) => project("upgrade", args2), daemon, doctor, run, ps, top, attach, send, questions, answer, log, board, tasks: tasksCommand, task: taskCommand, formula: formulaCommand, permissions: permissionsCommand, agents: (args2) => libraryCommand("agents", args2), skills: (args2) => libraryCommand("skills", args2), mcp: (args2) => ["add", "rm", "mv", "cp", "test", "show", "edit"].includes(args2[0]) ? editCommand("mcp", args2) : libraryCommand("mcp", args2), hooks: (args2) => libraryCommand("hooks", args2), providers: (args2) => libraryCommand("providers", args2), provider: (args2) => editCommand("providers", args2), teams: teamsCommand, trust: trustCommand, agent: (args2) => editCommand("agents", args2), team: (args2) => editCommand("teams", args2), skill: (args2) => editCommand("skills", args2), hook: (args2) => editCommand("hooks", args2), verify, recall, pause, resume, language, interrupt, acp: acpCommand, web: webCommand, backup: backupCommand, restore: restoreCommand };
16
16
  var [command, ...args] = process.argv.slice(2);
17
17
  try {
18
- if (!Object.hasOwn(commands, command)) throw new UsageError();
19
- await commands[command](args);
18
+ if (command === "--version") {
19
+ console.log(true ? "0.0.1-beta.3" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
20
+ } else {
21
+ if (!Object.hasOwn(commands, command)) throw new UsageError();
22
+ await commands[command](args);
23
+ }
20
24
  } catch (error) {
21
25
  if (error instanceof UsageError || error?.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") console.error(USAGE);
22
26
  else console.error(`alp: ${scrubText(error?.message ?? error)}`);
@@ -3,7 +3,7 @@ import { access } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { installedProgram, installService, serviceFor, uninstallService } from "@anhlp/alp-client/service";
5
5
  import { holdDaemon, startDaemon } from "@anhlp/alp-client/supervise";
6
- import { alpHome, connect, lockAlive, readLock } from "@anhlp/alp-client/index";
6
+ import { alpHome, connect, lockAlive, readLock, withDaemonLifecycle } from "@anhlp/alp-client/index";
7
7
  import { scrubText } from "@anhlp/alp-core/log";
8
8
  import { UsageError } from "./usage.js";
9
9
  import { DAEMON_ENTRY } from "./paths.js";
@@ -52,8 +52,12 @@ async function daemon([action, ...rest]) {
52
52
  const lock = await readLock(alpHome());
53
53
  console.log(`alpd ${lock.version} running (pid ${lock.pid}) on ${socket}`);
54
54
  } else if (action === "stop") {
55
- const stopped = await stop();
56
55
  await holdDaemon(alpHome());
56
+ const stopped = await withDaemonLifecycle(alpHome(), "stop", async () => {
57
+ const stopped2 = await stop();
58
+ await holdDaemon(alpHome());
59
+ return stopped2;
60
+ });
57
61
  console.log(stopped ? `alpd stopped; it stays stopped until alp daemon start, a prompt in Paseo, or ${await installed() ? "the next login" : "Paseo starting again"}` : "alpd is not running");
58
62
  } else if (action === "restart") {
59
63
  const client = await running().catch(() => void 0);
@@ -68,17 +68,32 @@ export function connect(socketPath: string, client?: {
68
68
  name: string;
69
69
  version: string;
70
70
  }, options?: import("./rpc/client.js").AlpRpcClientOptions & import("./ipc.js").SocketTransportOptions): Promise<AlpClient>;
71
+ export function readDaemonLifecycle(home: any): Promise<{
72
+ token: string;
73
+ pid: number;
74
+ bootTime: number;
75
+ action: string;
76
+ } | undefined>;
77
+ /** Serializes requested starts and stops, including the time before alpd has a lock.
78
+ * Rename publishes a fully populated directory: an existing nonempty directory cannot
79
+ * be replaced. Reaping/release unlinks only its unique owner token, then rmdir; even
80
+ * racing stale reapers cannot remove a successor's nonempty directory.
81
+ */
82
+ export function withDaemonLifecycle(home: any, action: any, run: any, { joinStart }?: {
83
+ joinStart?: boolean | undefined;
84
+ }): Promise<any>;
71
85
  /**
72
86
  * Returns the socket of a ready daemon, starting one when none runs.
73
87
  * `entry` is the daemon script; it is started through its own detaching launcher so the
74
88
  * daemon outlives the client that started it.
75
89
  */
76
- export function ensureDaemon({ home, entry, env, execPath, timeoutMs }?: {
90
+ export function ensureDaemon({ home, entry, env, execPath, timeoutMs, lifecycleLocked }?: {
77
91
  home?: string | undefined;
78
92
  entry?: string | undefined;
79
93
  env?: NodeJS.ProcessEnv | undefined;
80
94
  execPath?: string | undefined;
81
95
  timeoutMs?: number | undefined;
96
+ lifecycleLocked?: boolean | undefined;
82
97
  }): Promise<any>;
83
98
  export function discoverIntegration(name?: string, options?: {}): Promise<string | undefined>;
84
99
  export function startIntegration(nameOrOptions?: string): Promise<any>;
@@ -1,7 +1,7 @@
1
1
  // packages/client/src/index.js
2
2
  import { spawn } from "node:child_process";
3
- import { createHash } from "node:crypto";
4
- import { open, readFile, realpath, stat, lstat, mkdir, readlink, unlink } from "node:fs/promises";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { open, readFile, writeFile, realpath, stat, lstat, mkdir, readlink, unlink, readdir, rename, rmdir, link } from "node:fs/promises";
5
5
  import net from "node:net";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
@@ -197,10 +197,86 @@ async function tail(file, lines = 30) {
197
197
  return "";
198
198
  }
199
199
  }
200
+ async function lifecycleOwners(home) {
201
+ const directory = path.join(home, "state", "alpd.starting");
202
+ const names = await readdir(directory).catch((error) => {
203
+ if (error.code === "ENOENT") return [];
204
+ throw error;
205
+ });
206
+ return names.map((token) => {
207
+ const match = /^owner-(\d+)-(\d+)-(start|stop)-[a-f0-9-]+$/.exec(token);
208
+ if (!match) throw new Error("Invalid alpd startup admission owner");
209
+ return { token, pid: Number(match[1]), bootTime: Number(match[2]), action: match[3] };
210
+ });
211
+ }
212
+ async function readDaemonLifecycle(home) {
213
+ return (await lifecycleOwners(home)).find((owner) => lockAlive(owner));
214
+ }
215
+ async function withDaemonLifecycle(home, action, run, { joinStart = false } = {}) {
216
+ try {
217
+ await assertPrivateDirectory(home, { create: true });
218
+ } catch (cause) {
219
+ throw Object.assign(new Error(`ALP_HOME_UNUSABLE, alpd failed to start while preparing its home: ${cause.message}`, { cause }), { code: "ALP_HOME_UNUSABLE" });
220
+ }
221
+ const state = path.join(home, "state");
222
+ await assertPrivateDirectory(state, { create: true });
223
+ const directory = path.join(state, "alpd.starting");
224
+ const token = `owner-${process.pid}-${bootTime()}-${action}-${randomUUID()}`;
225
+ const candidate = path.join(state, `alpd.starting-${token}`);
226
+ await mkdir(candidate, { mode: 448 });
227
+ await writeFile(path.join(candidate, token), JSON.stringify({ pid: process.pid, bootTime: bootTime(), action }), { mode: 384 });
228
+ try {
229
+ let joined = false;
230
+ if (joinStart) {
231
+ const owner = await readDaemonLifecycle(home);
232
+ if (owner?.action === "start") {
233
+ try {
234
+ await link(path.join(directory, owner.token), path.join(directory, token));
235
+ joined = true;
236
+ } catch (error) {
237
+ if (error.code !== "ENOENT") throw error;
238
+ }
239
+ }
240
+ }
241
+ while (!joined) {
242
+ try {
243
+ await rename(candidate, directory);
244
+ break;
245
+ } catch (error) {
246
+ if (!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(error.code)) throw error;
247
+ for (const owner of await lifecycleOwners(home)) {
248
+ if (!lockAlive(owner)) await unlink(path.join(directory, owner.token)).catch((error2) => {
249
+ if (error2.code !== "ENOENT") throw error2;
250
+ });
251
+ }
252
+ await rmdir(directory).catch((error2) => {
253
+ if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error2.code)) throw error2;
254
+ });
255
+ await new Promise((resolve) => setTimeout(resolve, 25));
256
+ }
257
+ }
258
+ try {
259
+ return await run();
260
+ } finally {
261
+ await unlink(path.join(directory, token));
262
+ await rmdir(directory).catch((error) => {
263
+ if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code)) throw error;
264
+ });
265
+ }
266
+ } finally {
267
+ await unlink(path.join(candidate, token)).catch((error) => {
268
+ if (error.code !== "ENOENT") throw error;
269
+ });
270
+ await rmdir(candidate).catch((error) => {
271
+ if (error.code !== "ENOENT") throw error;
272
+ });
273
+ }
274
+ }
200
275
  async function ensureDaemon({ home = alpHome(), entry = (
201
276
  /** @type {string | undefined} */
202
277
  void 0
203
- ), env = process.env, execPath = process.execPath, timeoutMs = 15e3 } = {}) {
278
+ ), env = process.env, execPath = process.execPath, timeoutMs = 15e3, lifecycleLocked = false } = {}) {
279
+ if (!lifecycleLocked) return withDaemonLifecycle(home, "start", () => ensureDaemon({ home, entry, env, execPath, timeoutMs, lifecycleLocked: true }));
204
280
  const ready = async () => {
205
281
  const lock = await readLock(home);
206
282
  return lockAlive(lock) && lock.ready ? lock.socket : void 0;
@@ -213,7 +289,7 @@ async function ensureDaemon({ home = alpHome(), entry = (
213
289
  entry ??= await findDaemonEntry({ home, env });
214
290
  if (!entry) throw new Error("ALP daemon is not running and alpd.js was not found. Install the ALP CLI and run `alp daemon start`, or set ALP_DAEMON_ENTRY to alpd.js");
215
291
  await new Promise((resolve, reject) => {
216
- const launcher = spawn(execPath, [entry, "--detach"], {
292
+ const launcher = spawn(execPath, [entry, "--detach", `--startup-timeout=${timeoutMs}`], {
217
293
  env: { ...env, ALP_HOME: home, ELECTRON_RUN_AS_NODE: "1" },
218
294
  stdio: ["ignore", "ignore", "pipe"],
219
295
  windowsHide: true
@@ -415,8 +491,10 @@ export {
415
491
  ensureDaemon,
416
492
  findDaemonEntry,
417
493
  lockAlive,
494
+ readDaemonLifecycle,
418
495
  readLock,
419
496
  redactUrl,
420
497
  releaseDaemonLock,
421
- startIntegration
498
+ startIntegration,
499
+ withDaemonLifecycle
422
500
  };
@@ -1,7 +1,7 @@
1
1
  // packages/client/src/supervise.js
2
2
  import { mkdir, unlink, writeFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { ensureDaemon, lockAlive, readLock } from "./index.js";
4
+ import { ensureDaemon, lockAlive, readLock, withDaemonLifecycle } from "./index.js";
5
5
  import { installedProgram, serviceFor, startService } from "./service.js";
6
6
  var holdFile = (home) => path.join(home, "state", "alpd.held");
7
7
  async function holdDaemon(home, by = "alp daemon stop") {
@@ -24,16 +24,20 @@ async function whenReady(home, timeoutMs) {
24
24
  }
25
25
  throw new Error(`alpd did not become ready within ${timeoutMs / 1e3} s; see ${path.join(home, "logs")}`);
26
26
  }
27
- async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3 }) {
28
- await releaseHold(home);
29
- const lock = await readLock(home);
30
- if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
31
- const service = serviceFor({ home, env });
32
- if (service && await installedProgram(service)) {
33
- await startService(service);
34
- return whenReady(home, timeoutMs);
35
- }
36
- return ensureDaemon({ home, entry, env, timeoutMs });
27
+ async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3, automatic = false }) {
28
+ return withDaemonLifecycle(home, "start", async () => {
29
+ if (automatic) {
30
+ if (await daemonHeld(home)) return;
31
+ } else await releaseHold(home);
32
+ const lock = await readLock(home);
33
+ if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
34
+ const service = serviceFor({ home, env });
35
+ if (service && await installedProgram(service)) {
36
+ await startService(service);
37
+ return whenReady(home, timeoutMs);
38
+ }
39
+ return ensureDaemon({ home, entry, env, timeoutMs, lifecycleLocked: true });
40
+ });
37
41
  }
38
42
  function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, misses = 2, log = () => {
39
43
  } }) {
@@ -41,7 +45,9 @@ function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, mis
41
45
  let missed = 0;
42
46
  let starting;
43
47
  const start = (reason) => {
44
- starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env }).then(() => log(`alpd ${reason}`), (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
48
+ starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env, automatic: reason !== "started with Paseo" }).then((socket) => {
49
+ if (socket) log(`alpd ${reason}`);
50
+ }, (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
45
51
  starting = void 0;
46
52
  missed = 0;
47
53
  });
@@ -1,3 +1,6 @@
1
+ /** Strict payload contract for opt-in project-data writes. Instructions carry prompt authority;
2
+ * executable configuration and references to it have no place in this policy. */
3
+ export function assertProjectDataContent(kind: any, content: any): void;
1
4
  /**
2
5
  * One entry: the version that applies (the project's, else the library's, else the
3
6
  * built-in), or one scope's version with `scope`. `revision` goes back with a save.
@@ -52,7 +55,7 @@ export function usersOf(kind: any, name: any, { root, library, templates }?: {})
52
55
  * that scope (null to create); a different current revision refuses the save.
53
56
  * @returns {Promise<{ revision: string | null }>}
54
57
  */
55
- export function saveEntry(kind: any, name: any, content: any, { scope, root, library, templates, revision }?: {}): Promise<{
58
+ export function saveEntry(kind: any, name: any, content: any, { scope, root, library, templates, revision, policy }?: {}): Promise<{
56
59
  revision: string | null;
57
60
  }>;
58
61
  /**
@@ -60,16 +63,16 @@ export function saveEntry(kind: any, name: any, content: any, { scope, root, lib
60
63
  * entry others refer to stays, and the error lists them.
61
64
  * @returns {Promise<{ removed: boolean, now?: 'builtin' | 'library' }>}
62
65
  */
63
- export function deleteEntry(kind: any, name: any, { scope, root, library, templates, revision }?: {}): Promise<{
66
+ export function deleteEntry(kind: any, name: any, { scope, root, library, templates, revision, policy }?: {}): Promise<{
64
67
  removed: boolean;
65
68
  now?: "builtin" | "library";
66
69
  }>;
67
70
  /** Copies the entry that applies under `from` to a new entry `to` in a scope; built-ins are copied this way. */
68
- export function duplicateEntry(kind: any, from: any, to: any, { scope, root, library, templates }?: {}): Promise<{
71
+ export function duplicateEntry(kind: any, from: any, to: any, { scope, root, library, templates, policy }?: {}): Promise<{
69
72
  revision: string | null;
70
73
  }>;
71
74
  /** Renames an entry within a scope; refused while others refer to it by its old name. */
72
- export function renameEntry(kind: any, from: any, to: any, { scope, root, library, templates }?: {}): Promise<{
75
+ export function renameEntry(kind: any, from: any, to: any, { scope, root, library, templates, policy }?: {}): Promise<{
73
76
  revision: string | null;
74
77
  }>;
75
78
  /**
@@ -1,5 +1,5 @@
1
1
  // packages/core/src/library-edit.js
2
- import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { moveFile } from "./atomic.js";
@@ -39,6 +39,42 @@ function base(scope, { root, library }, kind) {
39
39
  return fail("INVALID_SCOPE", `Scope must be library or project, not '${scope}'`);
40
40
  }
41
41
  var revisionOf = (content) => content === void 0 ? null : createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16);
42
+ function assertProjectDataContent(kind, content) {
43
+ const deny = () => fail("UNSAFE_LIBRARY_WRITE", "Project-data writes allow only instruction entries and non-executable agent configuration");
44
+ if (!object(content) || !["agents", "skills"].includes(kind)) deny();
45
+ const keys = kind === "skills" ? ["body"] : ["instructions", "config"];
46
+ if (Object.keys(content).some((key) => !keys.includes(key))) deny();
47
+ const text = kind === "skills" ? content.body : content.instructions;
48
+ if (typeof text !== "string" || !text.trim()) deny();
49
+ if (content.config !== void 0) {
50
+ const config = content.config;
51
+ if (!object(config)) deny();
52
+ const allowed = ["description", "model", "thinking", "context", "autocompact"];
53
+ if (Object.keys(config).some((key) => !allowed.includes(key))) deny();
54
+ for (const key of ["description", "thinking"]) if (config[key] !== void 0 && typeof config[key] !== "string") deny();
55
+ if (config.model !== void 0 && (typeof config.model !== "string" || !/^(codex|claude):[^\s/\\:]{1,240}$/.test(config.model))) deny();
56
+ for (const key of ["context", "autocompact"]) if (config[key] !== void 0 && !(typeof config[key] === "string" || typeof config[key] === "number" && Number.isFinite(config[key]))) deny();
57
+ }
58
+ }
59
+ async function projectDataEntry(kind, name, { scope, root, library, templates }) {
60
+ if (scope !== "project" || !root || !["agents", "skills"].includes(kind)) fail("UNSAFE_LIBRARY_WRITE", "Project-data writes require project scope and agents or skills");
61
+ nameOf(name);
62
+ const directory = base(scope, { root, library }, kind);
63
+ await safeEntryPaths(directory, kind, name);
64
+ const found = await layers(kind, name, { root, library, templates });
65
+ if (found.library !== void 0 || found.builtin !== void 0) fail("UNSAFE_LIBRARY_WRITE", "Project-data writes cannot shadow or reveal a library or built-in entry");
66
+ if (found.project !== void 0) assertProjectDataContent(kind, found.project);
67
+ const owned = path.join(directory, kind, name);
68
+ const files = await readdir(owned).catch((error) => {
69
+ if (error.code === "ENOENT") return [];
70
+ throw error;
71
+ });
72
+ const allowed = new Set(FILES[kind](name).map(([file]) => path.basename(file)));
73
+ for (const file of files) {
74
+ const info = await lstat(path.join(owned, file));
75
+ if (!allowed.has(file) || !info.isFile() || info.isSymbolicLink() || info.nlink !== 1) fail("UNSAFE_LIBRARY_WRITE", "Project-data entries cannot contain sidecars, links or executable assets");
76
+ }
77
+ }
42
78
  async function safeEntryPaths(directory, kind, name) {
43
79
  for (const [file] of FILES[kind](name)) {
44
80
  await assertNoSymlinks(directory, path.join(kind, file));
@@ -236,9 +272,13 @@ async function writeAtomic(file, text) {
236
272
  await moveFile(temporary, file);
237
273
  }
238
274
  var exists = (target) => stat(target).then(() => true, () => false);
239
- async function saveEntry(kind, name, content, { scope, root, library, templates, revision } = {}) {
275
+ async function saveEntry(kind, name, content, { scope, root, library, templates, revision, policy } = {}) {
240
276
  kindOf(kind);
241
277
  nameOf(name);
278
+ if (policy === "project-data") {
279
+ assertProjectDataContent(kind, content);
280
+ await projectDataEntry(kind, name, { scope, root, library, templates });
281
+ }
242
282
  const directory = base(scope, { root, library }, kind);
243
283
  await safeEntryPaths(directory, kind, name);
244
284
  await check(kind, name, content, { scope, root, library, templates });
@@ -260,9 +300,10 @@ async function saveEntry(kind, name, content, { scope, root, library, templates,
260
300
  }
261
301
  return { revision: revisionOf(await readLayer(kind, name, directory)) };
262
302
  }
263
- async function deleteEntry(kind, name, { scope, root, library, templates, revision } = {}) {
303
+ async function deleteEntry(kind, name, { scope, root, library, templates, revision, policy } = {}) {
264
304
  kindOf(kind);
265
305
  nameOf(name);
306
+ if (policy === "project-data") await projectDataEntry(kind, name, { scope, root, library, templates });
266
307
  const directory = base(scope, { root, library }, kind);
267
308
  const current = await readLayer(kind, name, directory);
268
309
  if (current === void 0) fail("NOT_FOUND", `No ${kind} entry '${name}' in the ${scope}`);
@@ -278,14 +319,25 @@ async function deleteEntry(kind, name, { scope, root, library, templates, revisi
278
319
  const now = scope === "project" && found.library !== void 0 ? "library" : "builtin";
279
320
  return { removed: true, ...fallback !== void 0 ? { now } : {} };
280
321
  }
281
- async function duplicateEntry(kind, from, to, { scope, root, library, templates } = {}) {
322
+ async function duplicateEntry(kind, from, to, { scope, root, library, templates, policy } = {}) {
323
+ kindOf(kind);
324
+ nameOf(from);
325
+ nameOf(to);
326
+ if (policy === "project-data") {
327
+ await projectDataEntry(kind, from, { scope, root, library, templates });
328
+ await projectDataEntry(kind, to, { scope, root, library, templates });
329
+ }
282
330
  const { content } = await getEntry(kind, from, { root, library, templates });
283
- return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null });
331
+ return saveEntry(kind, to, structuredClone(content), { scope, root, library, templates, revision: null, policy });
284
332
  }
285
- async function renameEntry(kind, from, to, { scope, root, library, templates } = {}) {
333
+ async function renameEntry(kind, from, to, { scope, root, library, templates, policy } = {}) {
286
334
  kindOf(kind);
287
335
  nameOf(from);
288
336
  nameOf(to);
337
+ if (policy === "project-data") {
338
+ await projectDataEntry(kind, from, { scope, root, library, templates });
339
+ await projectDataEntry(kind, to, { scope, root, library, templates });
340
+ }
289
341
  const directory = base(scope, { root, library }, kind);
290
342
  const content = await readLayer(kind, from, directory);
291
343
  if (content === void 0) fail("NOT_FOUND", `No ${kind} entry '${from}' in the ${scope}`);
@@ -303,6 +355,7 @@ async function renameEntry(kind, from, to, { scope, root, library, templates } =
303
355
  }
304
356
  export {
305
357
  EDIT_KINDS,
358
+ assertProjectDataContent,
306
359
  deleteEntry,
307
360
  duplicateEntry,
308
361
  getEntry,