narraeon 0.1.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.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +164 -0
  3. package/README.zh-CN.md +164 -0
  4. package/dist/node/cli/NarraeonCli.js +163 -0
  5. package/dist/node/cli/main.js +38 -0
  6. package/dist/node/protocol/appPreferences.js +5 -0
  7. package/dist/node/protocol/contentTree.js +14 -0
  8. package/dist/node/protocol/modelConnections.js +110 -0
  9. package/dist/node/protocol/playerViews.js +1 -0
  10. package/dist/node/protocol/v1.js +278 -0
  11. package/dist/node/runtime/V1Runtime.js +625 -0
  12. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
  14. package/dist/node/runtime/config/appPaths.js +19 -0
  15. package/dist/node/runtime/content/ContentLimits.js +1 -0
  16. package/dist/node/runtime/content/ContentTreeFile.js +1 -0
  17. package/dist/node/runtime/content/ContentWorkspace.js +601 -0
  18. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
  19. package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
  20. package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
  21. package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
  22. package/dist/node/runtime/model/AiFailureLog.js +332 -0
  23. package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
  24. package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
  25. package/dist/node/runtime/model/ModelHost.js +89 -0
  26. package/dist/node/runtime/model/ModelHostStream.js +367 -0
  27. package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
  28. package/dist/node/runtime/model/ProviderStream.js +90 -0
  29. package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
  30. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
  31. package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
  32. package/dist/node/runtime/play/PlayCallChain.js +1280 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
  36. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
  37. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
  38. package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
  39. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
  40. package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
  41. package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
  42. package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
  43. package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
  44. package/dist/node/runtime/world/WorldStateRevision.js +75 -0
  45. package/dist/node/server/createRuntime.js +6 -0
  46. package/dist/node/server/createServer.js +218 -0
  47. package/dist/node/server/main.js +25 -0
  48. package/dist/node/server/startWebServer.js +28 -0
  49. package/dist/node/shared/default-play-prompts.js +85 -0
  50. package/dist/node/shared/default-preset-host.js +538 -0
  51. package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
  52. package/dist/node/shared/first-party-action-choices.js +164 -0
  53. package/dist/node/shared/first-party-generic-panels.js +105 -0
  54. package/dist/node/shared/first-party-play-preset-templates.js +41 -0
  55. package/dist/node/shared/first-party-player-view.js +179 -0
  56. package/dist/node/shared/play-preset-prompt-roles.js +4 -0
  57. package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
  58. package/dist/web/assets/index-DDx1itzT.js +1192 -0
  59. package/dist/web/index.html +14 -0
  60. package/package.json +86 -0
@@ -0,0 +1,721 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync, unlinkSync } from "node:fs";
3
+ import { link, open, mkdir, readFile, readdir, rename, rm, stat, unlink, } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, join, posix, win32 } from "node:path";
5
+ import { portableContentTreePathKey } from "../../protocol/contentTree.js";
6
+ import { defaultContentWorkspaceLimits, } from "./ContentLimits.js";
7
+ import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, worldDocumentTextRequirement, } from "./FileNativeContentTree.js";
8
+ const localIdPattern = /^package-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
9
+ const localMetadataFileName = "local.json";
10
+ const currentTreeFileName = "current-tree.json";
11
+ const currentTreePreviousFileName = ".current-tree.previous.json";
12
+ const defaultLimits = defaultContentWorkspaceLimits;
13
+ export class CurrentTreeContentPackageNotFoundError extends Error {
14
+ constructor() {
15
+ super("The requested content package was not found");
16
+ this.name = "CurrentTreeContentPackageNotFoundError";
17
+ }
18
+ }
19
+ export class CurrentTreeContentLibraryError extends Error {
20
+ constructor(message, options) {
21
+ super(message, options);
22
+ this.name = "CurrentTreeContentLibraryError";
23
+ }
24
+ }
25
+ export class CurrentTreeContentPackageBusyError extends CurrentTreeContentLibraryError {
26
+ constructor() {
27
+ super("Another source-package save or model operation is still running");
28
+ this.name = "CurrentTreeContentPackageBusyError";
29
+ }
30
+ }
31
+ export class InvalidContentTreeError extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "InvalidContentTreeError";
35
+ }
36
+ }
37
+ /**
38
+ * Persists one logical current tree per local package. The tree is stored as a
39
+ * single replaceable envelope so saving cannot expose half of two file sets.
40
+ */
41
+ export class CurrentTreeContentLibrary {
42
+ #locksRoot;
43
+ #packagesRoot;
44
+ #stagingRoot;
45
+ #limits;
46
+ #beforePackagePublish;
47
+ #beforeCurrentTreeReplace;
48
+ #mutationTail = Promise.resolve();
49
+ constructor(dataRoot, options = {}) {
50
+ this.#locksRoot = join(dataRoot, "content", ".package-locks");
51
+ this.#packagesRoot = join(dataRoot, "content", "packages");
52
+ this.#stagingRoot = join(dataRoot, "content", ".package-staging");
53
+ this.#limits = normalizeLimits(options.limits);
54
+ this.#beforePackagePublish = options.beforePackagePublish;
55
+ this.#beforeCurrentTreeReplace = options.beforeCurrentTreeReplace;
56
+ }
57
+ createPackage(files = []) {
58
+ return this.#mutate(async () => {
59
+ const normalized = normalizeCurrentContentTreeFiles(files, this.#limits);
60
+ const package_ = await this.#publishNewPackage(normalized);
61
+ return packageSummary(package_);
62
+ });
63
+ }
64
+ importPackage(files, options = {}) {
65
+ return this.#mutate(async () => {
66
+ options.signal?.throwIfAborted();
67
+ const normalized = normalizeCurrentContentTreeFiles(files, this.#limits);
68
+ return this.#publishNewPackage(normalized, options.signal);
69
+ });
70
+ }
71
+ copyPackage(localId, options = {}) {
72
+ return this.#mutate(async () => {
73
+ options.signal?.throwIfAborted();
74
+ const source = await this.readPackage(localId);
75
+ options.signal?.throwIfAborted();
76
+ return this.#publishNewPackage(source.files, options.signal);
77
+ });
78
+ }
79
+ deletePackage(localId, options = {}) {
80
+ return this.#mutate(async () => {
81
+ const lease = await this.acquireOperationLease(localId, {
82
+ wait: options.waitForLease !== false,
83
+ });
84
+ try {
85
+ options.signal?.throwIfAborted();
86
+ await mkdir(this.#stagingRoot, { recursive: true, mode: 0o700 });
87
+ const tombstonePath = join(this.#stagingRoot, `.deleted-${localId}-${randomUUID()}`);
88
+ try {
89
+ options.signal?.throwIfAborted();
90
+ await rename(this.#packageRoot(localId), tombstonePath);
91
+ await syncDirectory(this.#packagesRoot);
92
+ }
93
+ catch (error) {
94
+ if (isAbortError(error))
95
+ throw error;
96
+ throw new CurrentTreeContentLibraryError("Unable to delete the content package atomically", {
97
+ cause: error,
98
+ });
99
+ }
100
+ await rm(tombstonePath, { force: true, recursive: true }).catch(() => undefined);
101
+ }
102
+ finally {
103
+ lease.release();
104
+ }
105
+ });
106
+ }
107
+ async listPackages() {
108
+ let entries;
109
+ try {
110
+ entries = await readdir(this.#packagesRoot, { withFileTypes: true });
111
+ }
112
+ catch (error) {
113
+ if (isNodeErrorCode(error, "ENOENT")) {
114
+ return [];
115
+ }
116
+ throw new CurrentTreeContentLibraryError("Unable to list content packages", {
117
+ cause: error,
118
+ });
119
+ }
120
+ const packages = [];
121
+ for (const entry of entries.sort((left, right) => compareStrings(left.name, right.name))) {
122
+ if (!entry.isDirectory() || !localIdPattern.test(entry.name)) {
123
+ throw new CurrentTreeContentLibraryError("The content-package library contains an unrecognized entry");
124
+ }
125
+ const detail = await this.readPackage(entry.name);
126
+ packages.push(packageSummary(detail));
127
+ }
128
+ return packages;
129
+ }
130
+ async readPackage(localId) {
131
+ const packageRoot = this.#packageRoot(localId);
132
+ const metadata = await readJsonFile(join(packageRoot, localMetadataFileName), () => new CurrentTreeContentPackageNotFoundError(), "The local content-package envelope is damaged");
133
+ if (!isStoredMetadata(metadata) || metadata.localId !== localId) {
134
+ throw new CurrentTreeContentLibraryError("The local content-package envelope is damaged");
135
+ }
136
+ const storedTree = await readJsonFile(join(packageRoot, currentTreeFileName), () => new CurrentTreeContentLibraryError("The content package has no current tree"), "The content package's current tree is damaged");
137
+ if (!isStoredCurrentTree(storedTree)) {
138
+ throw new CurrentTreeContentLibraryError("The content package's current tree is damaged");
139
+ }
140
+ let files;
141
+ try {
142
+ files = normalizeCurrentContentTreeFiles(storedTree.files, this.#limits);
143
+ }
144
+ catch (error) {
145
+ throw new CurrentTreeContentLibraryError("The content package's current tree is damaged", {
146
+ cause: error,
147
+ });
148
+ }
149
+ return detail(localId, files, inspectContentPackageCurrentTree(files), metadata.name);
150
+ }
151
+ /**
152
+ * Name a package in the local library. The name lives in the package's own
153
+ * local shell, not in its content, so renaming never touches a document the
154
+ * runtime reads during play.
155
+ */
156
+ renamePackage(localId, name, options = {}) {
157
+ return this.#mutate(async () => {
158
+ const trimmed = name.trim();
159
+ if (!validPackageName(trimmed))
160
+ throw new CurrentTreeContentLibraryError("A content-package name must contain 1 to 160 characters and no line breaks");
161
+ const lease = await this.acquireOperationLease(localId, {
162
+ wait: options.waitForLease !== false,
163
+ });
164
+ try {
165
+ options.signal?.throwIfAborted();
166
+ const packageRoot = this.#packageRoot(localId);
167
+ const metadata = await readJsonFile(join(packageRoot, localMetadataFileName), () => new CurrentTreeContentPackageNotFoundError(), "The local content-package envelope is damaged");
168
+ if (!isStoredMetadata(metadata) || metadata.localId !== localId)
169
+ throw new CurrentTreeContentLibraryError("The local content-package envelope is damaged");
170
+ const temporaryPath = join(packageRoot, `.local-${randomUUID()}.tmp`);
171
+ try {
172
+ await writeDurableNewFile(temporaryPath, serialize({
173
+ schemaVersion: 1,
174
+ localId,
175
+ name: trimmed,
176
+ }));
177
+ await rename(temporaryPath, join(packageRoot, localMetadataFileName));
178
+ await syncDirectory(packageRoot);
179
+ }
180
+ catch (error) {
181
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
182
+ throw error;
183
+ }
184
+ return packageSummary(await this.readPackage(localId));
185
+ }
186
+ finally {
187
+ lease.release();
188
+ }
189
+ });
190
+ }
191
+ replaceCurrentTree(localId, files, options = {}) {
192
+ return this.#mutate(async () => {
193
+ const lease = await this.acquireOperationLease(localId, {
194
+ wait: options.waitForLease !== false,
195
+ });
196
+ try {
197
+ return await lease.replace(files, options);
198
+ }
199
+ finally {
200
+ lease.release();
201
+ }
202
+ });
203
+ }
204
+ async acquireOperationLease(localId, options = {}) {
205
+ this.#packageRoot(localId);
206
+ await mkdir(this.#locksRoot, { recursive: true, mode: 0o700 });
207
+ const lockPath = this.#packageLockPath(localId);
208
+ const reclaimPath = this.#packageReclaimPath(localId);
209
+ await clearDeadPackageLockMarker(reclaimPath);
210
+ if (await pathExists(reclaimPath)) {
211
+ throw new CurrentTreeContentPackageBusyError();
212
+ }
213
+ const marker = createPackageLockMarker();
214
+ const temporaryPath = join(this.#locksRoot, `.${localId}.${marker.token}.tmp`);
215
+ let handle;
216
+ try {
217
+ handle = await open(temporaryPath, "wx", 0o600);
218
+ await handle.writeFile(`${JSON.stringify(marker)}\n`, "utf8");
219
+ await handle.sync();
220
+ await handle.close();
221
+ handle = undefined;
222
+ try {
223
+ await link(temporaryPath, lockPath);
224
+ }
225
+ catch (error) {
226
+ if (!isNodeErrorCode(error, "EEXIST"))
227
+ throw error;
228
+ if (options.wait === true) {
229
+ await this.#waitForPackageLock(lockPath, reclaimPath, temporaryPath);
230
+ }
231
+ else if (!(await this.#tryReplaceDeadPackageLock(lockPath, reclaimPath, temporaryPath))) {
232
+ throw new CurrentTreeContentPackageBusyError();
233
+ }
234
+ }
235
+ }
236
+ catch (error) {
237
+ await handle?.close().catch(() => undefined);
238
+ if (error instanceof CurrentTreeContentPackageBusyError)
239
+ throw error;
240
+ throw new CurrentTreeContentLibraryError("Unable to acquire the source-package save owner", {
241
+ cause: error instanceof Error ? error : undefined,
242
+ });
243
+ }
244
+ finally {
245
+ await unlink(temporaryPath).catch(() => undefined);
246
+ }
247
+ let released = false;
248
+ try {
249
+ const package_ = await this.readPackage(localId);
250
+ return {
251
+ package: structuredClone(package_),
252
+ replace: async (files, options = {}) => {
253
+ if (released)
254
+ throw new CurrentTreeContentPackageBusyError();
255
+ options.signal?.throwIfAborted();
256
+ const normalized = normalizeCurrentContentTreeFiles(files, this.#limits);
257
+ return this.#replaceNormalizedCurrentTree(localId, normalized, options.signal);
258
+ },
259
+ release: () => {
260
+ if (released)
261
+ return;
262
+ released = true;
263
+ releasePackageOperationLockSync(lockPath, marker.token);
264
+ },
265
+ };
266
+ }
267
+ catch (error) {
268
+ releasePackageOperationLockSync(lockPath, marker.token);
269
+ throw error;
270
+ }
271
+ }
272
+ async #replaceNormalizedCurrentTree(localId, normalized, signal) {
273
+ const packageRoot = this.#packageRoot(localId);
274
+ const temporaryPath = join(packageRoot, `.current-tree-${randomUUID()}.tmp`);
275
+ const previousPath = join(packageRoot, currentTreePreviousFileName);
276
+ try {
277
+ signal?.throwIfAborted();
278
+ await writeDurableNewFile(temporaryPath, serialize({
279
+ schemaVersion: 1,
280
+ files: normalized,
281
+ }));
282
+ await crashProcessAtCurrentTreeEdge("after_current_tree_staging");
283
+ signal?.throwIfAborted();
284
+ await this.#beforeCurrentTreeReplace?.();
285
+ signal?.throwIfAborted();
286
+ await unlink(previousPath).catch((error) => {
287
+ if (!isNodeErrorCode(error, "ENOENT"))
288
+ throw error;
289
+ });
290
+ await link(join(packageRoot, currentTreeFileName), previousPath);
291
+ await syncDirectory(packageRoot);
292
+ await rename(temporaryPath, join(packageRoot, currentTreeFileName));
293
+ await syncDirectory(packageRoot);
294
+ await crashProcessAtCurrentTreeEdge("after_current_tree_replace");
295
+ failCurrentTreeAfterReplaceForTest();
296
+ await unlink(previousPath).catch(() => undefined);
297
+ }
298
+ catch (error) {
299
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
300
+ let rollbackFailure;
301
+ if (await pathExists(previousPath).catch(() => false)) {
302
+ try {
303
+ await rename(previousPath, join(packageRoot, currentTreeFileName));
304
+ await syncDirectory(packageRoot);
305
+ }
306
+ catch (rollbackError) {
307
+ rollbackFailure = rollbackError;
308
+ }
309
+ }
310
+ if (rollbackFailure !== undefined) {
311
+ throw new CurrentTreeContentLibraryError("Unable to restore the content package's previous current tree", {
312
+ cause: rollbackFailure instanceof Error ? rollbackFailure : undefined,
313
+ });
314
+ }
315
+ if (error instanceof InvalidContentTreeError || isAbortError(error)) {
316
+ throw error;
317
+ }
318
+ throw new CurrentTreeContentLibraryError(error instanceof Error
319
+ ? error.message
320
+ : "Unable to replace the content package's current tree atomically", { cause: error });
321
+ }
322
+ return detail(localId, normalized, inspectContentPackageCurrentTree(normalized));
323
+ }
324
+ async #publishNewPackage(normalized, signal) {
325
+ const localId = `package-${randomUUID()}`;
326
+ const stagingPath = join(this.#stagingRoot, localId);
327
+ const finalPath = join(this.#packagesRoot, localId);
328
+ await Promise.all([
329
+ mkdir(this.#packagesRoot, { recursive: true, mode: 0o700 }),
330
+ mkdir(this.#stagingRoot, { recursive: true, mode: 0o700 }),
331
+ ]);
332
+ try {
333
+ signal?.throwIfAborted();
334
+ await mkdir(stagingPath, { mode: 0o700 });
335
+ await writeDurableNewFile(join(stagingPath, localMetadataFileName), serialize({
336
+ schemaVersion: 1,
337
+ localId,
338
+ }));
339
+ await writeDurableNewFile(join(stagingPath, currentTreeFileName), serialize({
340
+ schemaVersion: 1,
341
+ files: normalized,
342
+ }));
343
+ await syncDirectory(stagingPath);
344
+ await crashProcessAtCurrentTreeEdge("after_package_staging");
345
+ signal?.throwIfAborted();
346
+ await this.#beforePackagePublish?.();
347
+ signal?.throwIfAborted();
348
+ await rename(stagingPath, finalPath);
349
+ await syncDirectory(this.#packagesRoot);
350
+ await crashProcessAtCurrentTreeEdge("after_package_publish");
351
+ }
352
+ catch (error) {
353
+ await rm(stagingPath, { force: true, recursive: true }).catch(() => undefined);
354
+ if (isAbortError(error))
355
+ throw error;
356
+ throw new CurrentTreeContentLibraryError("Unable to publish the content package atomically", {
357
+ cause: error,
358
+ });
359
+ }
360
+ return detail(localId, normalized, inspectContentPackageCurrentTree(normalized));
361
+ }
362
+ #packageRoot(localId) {
363
+ if (!localIdPattern.test(localId)) {
364
+ throw new CurrentTreeContentPackageNotFoundError();
365
+ }
366
+ return join(this.#packagesRoot, localId);
367
+ }
368
+ #packageLockPath(localId) {
369
+ return join(this.#locksRoot, `${localId}.mutation`);
370
+ }
371
+ #packageReclaimPath(localId) {
372
+ return join(this.#locksRoot, `${localId}.reclaim`);
373
+ }
374
+ async #tryReplaceDeadPackageLock(lockPath, reclaimPath, replacementPath) {
375
+ const current = await readPackageLockMarker(lockPath);
376
+ if (current === null || isProcessAlive(current.pid))
377
+ return false;
378
+ const reclaim = createPackageLockMarker();
379
+ const temporaryPath = join(this.#locksRoot, `.reclaim.${reclaim.token}.tmp`);
380
+ let handle;
381
+ try {
382
+ handle = await open(temporaryPath, "wx", 0o600);
383
+ await handle.writeFile(`${JSON.stringify(reclaim)}\n`, "utf8");
384
+ await handle.sync();
385
+ await handle.close();
386
+ handle = undefined;
387
+ await link(temporaryPath, reclaimPath);
388
+ }
389
+ catch (error) {
390
+ await handle?.close().catch(() => undefined);
391
+ if (isNodeErrorCode(error, "EEXIST"))
392
+ return false;
393
+ throw error;
394
+ }
395
+ finally {
396
+ await unlink(temporaryPath).catch(() => undefined);
397
+ }
398
+ try {
399
+ const verified = await readPackageLockMarker(lockPath);
400
+ if (verified?.token !== current.token || isProcessAlive(current.pid)) {
401
+ return false;
402
+ }
403
+ await unlink(lockPath).catch((error) => {
404
+ if (!isNodeErrorCode(error, "ENOENT"))
405
+ throw error;
406
+ });
407
+ await link(replacementPath, lockPath);
408
+ return true;
409
+ }
410
+ finally {
411
+ const owner = await readPackageLockMarker(reclaimPath);
412
+ if (owner?.token === reclaim.token) {
413
+ await unlink(reclaimPath).catch(() => undefined);
414
+ }
415
+ }
416
+ }
417
+ async #waitForPackageLock(lockPath, reclaimPath, replacementPath) {
418
+ for (let attempt = 0; attempt < 2_000; attempt += 1) {
419
+ if ((await this.#tryReplaceDeadPackageLock(lockPath, reclaimPath, replacementPath)) ||
420
+ (await link(replacementPath, lockPath)
421
+ .then(() => true)
422
+ .catch((error) => {
423
+ if (isNodeErrorCode(error, "EEXIST"))
424
+ return false;
425
+ throw error;
426
+ }))) {
427
+ return;
428
+ }
429
+ await new Promise((resolve) => setTimeout(resolve, 5));
430
+ }
431
+ throw new CurrentTreeContentPackageBusyError();
432
+ }
433
+ async #mutate(operation) {
434
+ const previous = this.#mutationTail;
435
+ let release = () => undefined;
436
+ this.#mutationTail = new Promise((resolve) => {
437
+ release = resolve;
438
+ });
439
+ await previous;
440
+ try {
441
+ return await operation();
442
+ }
443
+ finally {
444
+ release();
445
+ }
446
+ }
447
+ }
448
+ function detail(localId, files, inspection, name) {
449
+ return {
450
+ ...summary(localId, inspection, name),
451
+ files: files.map((file) => ({ ...file })),
452
+ documents: structuredClone(inspection.documents),
453
+ issues: structuredClone(inspection.issues),
454
+ opening: inspection.opening,
455
+ control: structuredClone(inspection.control),
456
+ };
457
+ }
458
+ function summary(localId, inspection, name) {
459
+ return {
460
+ localId,
461
+ // An author-chosen name wins; without one the name still tracks whatever
462
+ // the package currently contains.
463
+ displayName: name ?? inspection.displayName,
464
+ status: inspection.status,
465
+ documentCount: inspection.documents.length,
466
+ issueCount: inspection.issues.length,
467
+ };
468
+ }
469
+ function packageSummary(package_) {
470
+ return {
471
+ localId: package_.localId,
472
+ displayName: package_.displayName,
473
+ status: package_.status,
474
+ documentCount: package_.documentCount,
475
+ issueCount: package_.issueCount,
476
+ };
477
+ }
478
+ export function normalizeCurrentContentTreeFiles(files, limits = defaultLimits) {
479
+ if (files.length > limits.maxFiles) {
480
+ throw new InvalidContentTreeError("The content package exceeds the file-count limit");
481
+ }
482
+ const normalized = [];
483
+ const portableKeys = new Set();
484
+ let totalBytes = 0;
485
+ for (const file of files) {
486
+ if (typeof file !== "object" ||
487
+ file === null ||
488
+ typeof file.path !== "string" ||
489
+ typeof file.contents !== "string" ||
490
+ (file.encoding !== undefined && file.encoding !== "base64")) {
491
+ throw new InvalidContentTreeError("Every content-package file must contain a path and text content");
492
+ }
493
+ const path = normalizeContentTreePath(file.path);
494
+ if (isContentPackageWorldDocumentPath(path) &&
495
+ file.encoding !== undefined) {
496
+ throw new InvalidContentTreeError(worldDocumentTextRequirement(path));
497
+ }
498
+ const leaf = basename(path).toLocaleLowerCase("en-US");
499
+ if (leaf === "manifest.json" || leaf.endsWith(".package.json")) {
500
+ throw new InvalidContentTreeError(`The current content tree does not allow manifest files: ${path}`);
501
+ }
502
+ const portableKey = portableContentTreePathKey(path);
503
+ if (portableKeys.has(portableKey)) {
504
+ throw new InvalidContentTreeError(`The content tree contains a duplicate normalized path: ${path}`);
505
+ }
506
+ portableKeys.add(portableKey);
507
+ const bytes = file.encoding === "base64"
508
+ ? decodeCanonicalBase64(file.contents, path).byteLength
509
+ : Buffer.byteLength(file.contents, "utf8");
510
+ if (bytes > limits.maxFileBytes) {
511
+ throw new InvalidContentTreeError(`A content-package file exceeds the size limit: ${path}`);
512
+ }
513
+ totalBytes += bytes;
514
+ if (totalBytes > limits.maxTotalBytes) {
515
+ throw new InvalidContentTreeError("The content package exceeds the total-size limit");
516
+ }
517
+ normalized.push({
518
+ path,
519
+ contents: file.contents,
520
+ ...(file.encoding === undefined ? {} : { encoding: file.encoding }),
521
+ });
522
+ }
523
+ return normalized.sort((left, right) => compareStrings(left.path, right.path));
524
+ }
525
+ function decodeCanonicalBase64(contents, path) {
526
+ if (contents.length % 4 !== 0 ||
527
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(contents)) {
528
+ throw new InvalidContentTreeError(`A binary content-package asset is not canonical Base64: ${path}`);
529
+ }
530
+ const decoded = Buffer.from(contents, "base64");
531
+ if (decoded.toString("base64") !== contents) {
532
+ throw new InvalidContentTreeError(`A binary content-package asset is not canonical Base64: ${path}`);
533
+ }
534
+ return decoded;
535
+ }
536
+ function normalizeContentTreePath(value) {
537
+ if (value.length === 0 ||
538
+ [...value].length > 512 ||
539
+ value.includes("\\") ||
540
+ value.includes("\0") ||
541
+ isAbsolute(value) ||
542
+ win32.isAbsolute(value)) {
543
+ throw new InvalidContentTreeError(`Invalid content-tree path: ${value}`);
544
+ }
545
+ const normalized = posix.normalize(value.normalize("NFC"));
546
+ const segments = normalized.split("/");
547
+ if (normalized === "." ||
548
+ normalized.startsWith("../") ||
549
+ Buffer.byteLength(normalized, "utf8") > 240 ||
550
+ segments.length > 16 ||
551
+ segments.some((segment) => segment.length === 0 ||
552
+ segment === "." ||
553
+ segment === ".." ||
554
+ Buffer.byteLength(segment, "utf8") > 80 ||
555
+ containsControlCharacter(segment))) {
556
+ throw new InvalidContentTreeError(`Invalid content-tree path: ${value}`);
557
+ }
558
+ return normalized;
559
+ }
560
+ function containsControlCharacter(value) {
561
+ return [...value].some((character) => {
562
+ const codePoint = character.codePointAt(0) ?? 0;
563
+ return codePoint <= 0x1f || codePoint === 0x7f;
564
+ });
565
+ }
566
+ function normalizeLimits(input) {
567
+ const limits = { ...defaultLimits, ...input };
568
+ for (const [name, value] of Object.entries(limits)) {
569
+ if (!Number.isSafeInteger(value) || value <= 0) {
570
+ throw new TypeError(`${name} must be a positive safe integer`);
571
+ }
572
+ }
573
+ if (limits.maxFileBytes > limits.maxTotalBytes) {
574
+ throw new TypeError("maxFileBytes must not exceed maxTotalBytes");
575
+ }
576
+ return limits;
577
+ }
578
+ async function writeDurableNewFile(path, contents) {
579
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
580
+ const handle = await open(path, "wx", 0o600);
581
+ try {
582
+ await handle.writeFile(contents, "utf8");
583
+ await handle.sync();
584
+ }
585
+ finally {
586
+ await handle.close();
587
+ }
588
+ }
589
+ async function syncDirectory(path) {
590
+ const handle = await open(path, "r");
591
+ try {
592
+ await handle.sync();
593
+ }
594
+ finally {
595
+ await handle.close();
596
+ }
597
+ }
598
+ async function readJsonFile(path, missing, invalidMessage) {
599
+ try {
600
+ return JSON.parse(await readFile(path, "utf8"));
601
+ }
602
+ catch (error) {
603
+ if (isNodeErrorCode(error, "ENOENT")) {
604
+ throw missing();
605
+ }
606
+ throw new CurrentTreeContentLibraryError(invalidMessage, { cause: error });
607
+ }
608
+ }
609
+ function isStoredMetadata(value) {
610
+ return (isPlainObject(value) &&
611
+ Object.keys(value).length === (value.name === undefined ? 2 : 3) &&
612
+ value.schemaVersion === 1 &&
613
+ typeof value.localId === "string" &&
614
+ localIdPattern.test(value.localId) &&
615
+ (value.name === undefined ||
616
+ (typeof value.name === "string" && validPackageName(value.name))));
617
+ }
618
+ const packageNamePattern = /^[^\r\n]{1,160}$/u;
619
+ function validPackageName(name) {
620
+ return packageNamePattern.test(name);
621
+ }
622
+ function isStoredCurrentTree(value) {
623
+ return (isPlainObject(value) &&
624
+ Object.keys(value).length === 2 &&
625
+ value.schemaVersion === 1 &&
626
+ Array.isArray(value.files));
627
+ }
628
+ function isPlainObject(value) {
629
+ return typeof value === "object" && value !== null && !Array.isArray(value);
630
+ }
631
+ function serialize(value) {
632
+ return `${JSON.stringify(value, null, 2)}\n`;
633
+ }
634
+ function createPackageLockMarker() {
635
+ return { schemaVersion: 1, pid: process.pid, token: randomUUID() };
636
+ }
637
+ async function readPackageLockMarker(path) {
638
+ try {
639
+ const value = JSON.parse(await readFile(path, "utf8"));
640
+ return parsePackageLockMarker(value);
641
+ }
642
+ catch {
643
+ return null;
644
+ }
645
+ }
646
+ function parsePackageLockMarker(value) {
647
+ if (isPlainObject(value) &&
648
+ value.schemaVersion === 1 &&
649
+ typeof value.pid === "number" &&
650
+ Number.isSafeInteger(value.pid) &&
651
+ value.pid > 0 &&
652
+ typeof value.token === "string" &&
653
+ /^[0-9a-f-]{36}$/u.test(value.token)) {
654
+ return { schemaVersion: 1, pid: value.pid, token: value.token };
655
+ }
656
+ return null;
657
+ }
658
+ function releasePackageOperationLockSync(path, token) {
659
+ try {
660
+ const current = parsePackageLockMarker(JSON.parse(readFileSync(path, "utf8")));
661
+ if (current?.token === token)
662
+ unlinkSync(path);
663
+ }
664
+ catch (error) {
665
+ if (!isNodeErrorCode(error, "ENOENT"))
666
+ throw error;
667
+ }
668
+ }
669
+ async function clearDeadPackageLockMarker(path) {
670
+ const marker = await readPackageLockMarker(path);
671
+ if ((marker !== null && !isProcessAlive(marker.pid)) ||
672
+ (marker === null && (await pathExists(path)))) {
673
+ await unlink(path).catch(() => undefined);
674
+ }
675
+ }
676
+ async function pathExists(path) {
677
+ try {
678
+ await stat(path);
679
+ return true;
680
+ }
681
+ catch (error) {
682
+ if (isNodeErrorCode(error, "ENOENT"))
683
+ return false;
684
+ throw error;
685
+ }
686
+ }
687
+ function isProcessAlive(pid) {
688
+ try {
689
+ process.kill(pid, 0);
690
+ return true;
691
+ }
692
+ catch (error) {
693
+ return !isNodeErrorCode(error, "ESRCH");
694
+ }
695
+ }
696
+ function isNodeErrorCode(error, code) {
697
+ return (typeof error === "object" &&
698
+ error !== null &&
699
+ "code" in error &&
700
+ error.code === code);
701
+ }
702
+ function isAbortError(error) {
703
+ return error instanceof Error && error.name === "AbortError";
704
+ }
705
+ async function crashProcessAtCurrentTreeEdge(edge) {
706
+ if (process.env.NODE_ENV !== "test" ||
707
+ process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_CURRENT_TREE_EDGE !== edge) {
708
+ return;
709
+ }
710
+ process.kill(process.pid, "SIGKILL");
711
+ await new Promise(() => undefined);
712
+ }
713
+ function failCurrentTreeAfterReplaceForTest() {
714
+ if (process.env.NODE_ENV === "test" &&
715
+ process.env.NARRAEON_INTERNAL_TEST_FAIL_AFTER_CURRENT_TREE_REPLACE === "1") {
716
+ throw new Error("simulated failure after current tree replace");
717
+ }
718
+ }
719
+ function compareStrings(left, right) {
720
+ return left < right ? -1 : left > right ? 1 : 0;
721
+ }