canopycms 0.8.2 → 0.9.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.
- package/CHANGELOG.md +79 -47
- package/dist/ai/json-to-markdown.js +2 -0
- package/dist/ai/resolve-branch.js +2 -1
- package/dist/api/branch-entry-changes.js +2 -4
- package/dist/authorization/branch.d.ts +10 -4
- package/dist/authorization/branch.js +24 -4
- package/dist/authorization/types.d.ts +1 -1
- package/dist/cli/cli.js +1984 -1630
- package/dist/cli/configured-asset-store.js +2 -8
- package/dist/cli/generate-ai-content.js +86 -16
- package/dist/cli/init.js +1 -1
- package/dist/cli/site-config.d.ts +9 -0
- package/dist/cli/site-config.js +17 -0
- package/dist/cli/sync.js +226 -74
- package/dist/config/types.d.ts +4 -4
- package/dist/config/types.js +1 -1
- package/dist/config/validation.d.ts +9 -3
- package/dist/config/validation.js +30 -5
- package/dist/content-listing.js +2 -3
- package/dist/content-store.js +5 -5
- package/dist/editor/FormRenderer.js +5 -0
- package/dist/editor/components/EntryChangesDrawer.js +3 -2
- package/dist/editor/entry-diff.js +7 -1
- package/dist/editor/fields/DateField.d.ts +19 -0
- package/dist/editor/fields/DateField.js +53 -0
- package/dist/editor/fields/DateTimeField.d.ts +10 -0
- package/dist/editor/fields/DateTimeField.js +5 -15
- package/dist/editor/fields/list-item-title.js +2 -1
- package/dist/editor/theme.js +12 -4
- package/dist/entry-schema-registry.js +2 -1
- package/dist/services.d.ts +3 -2
- package/dist/static/index.js +5 -2
- package/dist/system-users.d.ts +2 -0
- package/dist/system-users.js +6 -1
- package/dist/utils/calendar-day.d.ts +23 -0
- package/dist/utils/calendar-day.js +102 -0
- package/dist/utils/content-serialize.d.ts +4 -2
- package/dist/utils/content-serialize.js +8 -6
- package/dist/validation/entry-validator.js +7 -1
- package/dist/validation/field-traversal.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli/cli.js
CHANGED
|
@@ -1349,7 +1349,7 @@ var init_init = __esm({
|
|
|
1349
1349
|
init_types = __esm2({
|
|
1350
1350
|
"dist/config/types.js"() {
|
|
1351
1351
|
"use strict";
|
|
1352
|
-
scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
|
|
1352
|
+
scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
|
|
1353
1353
|
markdownFieldTypes = ["markdown", "mdx"];
|
|
1354
1354
|
primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
|
|
1355
1355
|
fieldTypes = [
|
|
@@ -3495,7 +3495,7 @@ async function atomicWriteFile2(filePath, content) {
|
|
|
3495
3495
|
}
|
|
3496
3496
|
}
|
|
3497
3497
|
function isPlainRecord(value) {
|
|
3498
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3498
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
3499
3499
|
}
|
|
3500
3500
|
function resolveBlockItem(blockField, item) {
|
|
3501
3501
|
const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
|
|
@@ -4068,6 +4068,20 @@ async function writeWorkerStatus(taskDir, report) {
|
|
|
4068
4068
|
function isBlockStructuralKey(key) {
|
|
4069
4069
|
return BLOCK_STRUCTURAL_KEYS.has(key);
|
|
4070
4070
|
}
|
|
4071
|
+
function withCalendarDays(fields, data) {
|
|
4072
|
+
let out = data;
|
|
4073
|
+
for (const field of fields) {
|
|
4074
|
+
if (field.type === "group") {
|
|
4075
|
+
out = withCalendarDays(field.fields, out);
|
|
4076
|
+
continue;
|
|
4077
|
+
}
|
|
4078
|
+
const value = out[field.name];
|
|
4079
|
+
const next = valueWithDays(field, value);
|
|
4080
|
+
if (next !== value)
|
|
4081
|
+
out = { ...out, [field.name]: next };
|
|
4082
|
+
}
|
|
4083
|
+
return out;
|
|
4084
|
+
}
|
|
4071
4085
|
function createDebugLogger2(options) {
|
|
4072
4086
|
return new DebugLogger2(options);
|
|
4073
4087
|
}
|
|
@@ -4711,9 +4725,9 @@ function asJson(value) {
|
|
|
4711
4725
|
const key = identityKey(value);
|
|
4712
4726
|
return key === void 0 || key === "undefined" ? void 0 : JSON.parse(key);
|
|
4713
4727
|
}
|
|
4714
|
-
function frontmatterAsRead(raw) {
|
|
4728
|
+
function frontmatterAsRead(raw, fields) {
|
|
4715
4729
|
try {
|
|
4716
|
-
return asJson(matter(raw, {}).data);
|
|
4730
|
+
return asJson(withCalendarDays(fields, matter(raw, {}).data));
|
|
4717
4731
|
} catch {
|
|
4718
4732
|
return void 0;
|
|
4719
4733
|
}
|
|
@@ -4781,7 +4795,7 @@ ${asKey ? `${text3}: v` : `v: ${text3}`}
|
|
|
4781
4795
|
return true;
|
|
4782
4796
|
}
|
|
4783
4797
|
}
|
|
4784
|
-
function serializeFrontmatter(editorBody, data, existingRaw, format) {
|
|
4798
|
+
function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []) {
|
|
4785
4799
|
const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
|
|
4786
4800
|
if (existingRaw === void 0)
|
|
4787
4801
|
return matter.stringify(file, data);
|
|
@@ -4789,7 +4803,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
|
|
|
4789
4803
|
if (existingFrontmatter === void 0)
|
|
4790
4804
|
return matter.stringify(file, data);
|
|
4791
4805
|
const onDisk = parseDocument2(existingFrontmatter);
|
|
4792
|
-
const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS()) : data;
|
|
4806
|
+
const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw, fields), onDisk.toJS()) : data;
|
|
4793
4807
|
const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord2(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
|
|
4794
4808
|
if (reconciled === void 0)
|
|
4795
4809
|
return matter.stringify(file, data);
|
|
@@ -4801,7 +4815,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
|
|
|
4801
4815
|
}
|
|
4802
4816
|
}
|
|
4803
4817
|
});
|
|
4804
|
-
if (isDeepStrictEqual2(frontmatterAsRead(written), asJson(data)))
|
|
4818
|
+
if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
|
|
4805
4819
|
return written;
|
|
4806
4820
|
log22.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
|
|
4807
4821
|
return matter.stringify(file, data);
|
|
@@ -6131,6 +6145,7 @@ _${field.description}_` : "";
|
|
|
6131
6145
|
switch (field.type) {
|
|
6132
6146
|
case "string":
|
|
6133
6147
|
case "number":
|
|
6148
|
+
case "date":
|
|
6134
6149
|
case "datetime":
|
|
6135
6150
|
return `${heading} ${label}${descriptionLine}
|
|
6136
6151
|
|
|
@@ -7905,7 +7920,7 @@ async function resolveBranchRoot(config) {
|
|
|
7905
7920
|
config: resolved,
|
|
7906
7921
|
branchName: activeBranch,
|
|
7907
7922
|
mode: config.mode,
|
|
7908
|
-
createdBy:
|
|
7923
|
+
createdBy: AI_USER_ID,
|
|
7909
7924
|
remoteUrl: config.defaultRemoteUrl
|
|
7910
7925
|
});
|
|
7911
7926
|
return context.branchRoot;
|
|
@@ -8054,6 +8069,11 @@ function validateScalar(field, value, path272) {
|
|
|
8054
8069
|
return [{ fieldPath: path272, message: "Expected true or false" }];
|
|
8055
8070
|
}
|
|
8056
8071
|
return [];
|
|
8072
|
+
case "date":
|
|
8073
|
+
if (value !== "" && !isCalendarDay(value)) {
|
|
8074
|
+
return [{ fieldPath: path272, message: "Expected a real date, written YYYY-MM-DD" }];
|
|
8075
|
+
}
|
|
8076
|
+
return [];
|
|
8057
8077
|
case "datetime":
|
|
8058
8078
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
8059
8079
|
return [{ fieldPath: path272, message: "Expected a valid date/time string" }];
|
|
@@ -8206,7 +8226,7 @@ function findInvalidEntries(items) {
|
|
|
8206
8226
|
for (const item of items) {
|
|
8207
8227
|
if (!item.schema)
|
|
8208
8228
|
continue;
|
|
8209
|
-
const data = normalizeDatesDeep(item.data);
|
|
8229
|
+
const data = normalizeDatesDeep(withCalendarDays(item.schema, item.data));
|
|
8210
8230
|
const errors = validateEntryData(item.schema, data);
|
|
8211
8231
|
if (errors.length > 0) {
|
|
8212
8232
|
invalid.push({ entryPath: item.entryPath, errors });
|
|
@@ -8434,7 +8454,7 @@ async function generateAIContentCLI(options) {
|
|
|
8434
8454
|
console.log(` Output: ${result.outputDir}
|
|
8435
8455
|
`);
|
|
8436
8456
|
}
|
|
8437
|
-
var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, editorUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, parseEntryParts, parseEntryData, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
|
|
8457
|
+
var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, RESERVED_SETTINGS_BRANCH_PREFIX2, init_branch_name, BASE58_PATTERN2, CONTENT_ID_PATTERN2, PHYSICAL_SEGMENT_PATTERN2, MAX_BRANCH_NAME_LENGTH2, init_validation, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceSortFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, editorUrlSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, seoFieldNameSchema2, seoSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned22, init_mode_env, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, warned4, DEPLOYMENT_PLACEHOLDER2, DEFAULT_BRANCH_NAMESPACE_TEMPLATE2, BRANCH_NAMESPACE_ENV2, init_branch_namespace, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, DAY, MS_PER_DAY, isRealDay, dayParts, isCalendarDay, dayOfBareDate, mapItems, blockItemWithDays, valueWithDays, entryAsRead, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, MARKDOWN_EXPORT_OPTIONS, BULLETS, DELIMITERS, EMPHASIS, STRONG, RULE, WORD_CHAR, GAP, Restyler, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned5, SchemaUnavailableError, isValidId, parseTypedFilename, EMPTY_LOGICAL_PATH, ENTRY_EXTENSIONS, ContentIdIndex, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SNAPSHOT_MEMO_MAX_BYTES, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, GitProcessError, STDERR_CAP_BYTES, KILL_GRACE_MS, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, HASH_INACTIVITY_TIMEOUT_MS, MAX_HASH_RUNS, LS_REMOTE_INACTIVITY_TIMEOUT_MS, symlinkBlobSha, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, BaseBranchMissingError, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, AI_USER_ID, log10, normalizePath, parseEntryParts, parseEntryData, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
|
|
8438
8458
|
var init_generate_ai_content = __esm({
|
|
8439
8459
|
"dist/cli/generate-ai-content.js"() {
|
|
8440
8460
|
"use strict";
|
|
@@ -8616,7 +8636,7 @@ var init_generate_ai_content = __esm({
|
|
|
8616
8636
|
init_types22 = __esm3({
|
|
8617
8637
|
"dist/config/types.js"() {
|
|
8618
8638
|
"use strict";
|
|
8619
|
-
scalarFieldTypes2 = ["string", "number", "boolean", "datetime", "code"];
|
|
8639
|
+
scalarFieldTypes2 = ["string", "number", "boolean", "date", "datetime", "code"];
|
|
8620
8640
|
markdownFieldTypes2 = ["markdown", "mdx"];
|
|
8621
8641
|
primitiveFieldTypes2 = [...scalarFieldTypes2, ...markdownFieldTypes2];
|
|
8622
8642
|
fieldTypes2 = [
|
|
@@ -9350,6 +9370,54 @@ var init_generate_ai_content = __esm({
|
|
|
9350
9370
|
BLOCK_DISCRIMINATOR_KEYS = ["template", "_type"];
|
|
9351
9371
|
BLOCK_STRUCTURAL_KEYS = new Set(BLOCK_DISCRIMINATOR_KEYS);
|
|
9352
9372
|
init_item_keys();
|
|
9373
|
+
init_item_keys();
|
|
9374
|
+
init_field_traversal();
|
|
9375
|
+
DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
9376
|
+
MS_PER_DAY = 864e5;
|
|
9377
|
+
isRealDay = (year, month, day) => {
|
|
9378
|
+
const lastOfMonth = /* @__PURE__ */ new Date(0);
|
|
9379
|
+
lastOfMonth.setUTCFullYear(year, month, 0);
|
|
9380
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= lastOfMonth.getUTCDate();
|
|
9381
|
+
};
|
|
9382
|
+
dayParts = (value) => {
|
|
9383
|
+
const match = typeof value === "string" ? DAY.exec(value) : null;
|
|
9384
|
+
if (!match)
|
|
9385
|
+
return void 0;
|
|
9386
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
9387
|
+
return isRealDay(year, month, day) ? [year, month, day] : void 0;
|
|
9388
|
+
};
|
|
9389
|
+
isCalendarDay = (value) => dayParts(value) !== void 0;
|
|
9390
|
+
dayOfBareDate = (value) => value.getTime() % MS_PER_DAY === 0 ? value.toISOString().slice(0, 10) : void 0;
|
|
9391
|
+
mapItems = (items, mapItem) => {
|
|
9392
|
+
const next = items.map((item) => isPlainRecord(item) ? mapItem(item) : item);
|
|
9393
|
+
return next.every((item, i) => item === items[i]) ? items : next;
|
|
9394
|
+
};
|
|
9395
|
+
blockItemWithDays = (field, item) => {
|
|
9396
|
+
const resolved = resolveBlockItem(field, item);
|
|
9397
|
+
if (!resolved)
|
|
9398
|
+
return item;
|
|
9399
|
+
const data = withCalendarDays(resolved.fields, resolved.data);
|
|
9400
|
+
if (data === resolved.data)
|
|
9401
|
+
return item;
|
|
9402
|
+
return resolved.data === item ? data : { ...item, value: data };
|
|
9403
|
+
};
|
|
9404
|
+
valueWithDays = (field, value) => {
|
|
9405
|
+
if (field.type === "date") {
|
|
9406
|
+
return value instanceof Date ? dayOfBareDate(value) ?? value : value;
|
|
9407
|
+
}
|
|
9408
|
+
if (field.type === "object") {
|
|
9409
|
+
const { fields, list } = field;
|
|
9410
|
+
if (list) {
|
|
9411
|
+
return Array.isArray(value) ? mapItems(value, (item) => withCalendarDays(fields ?? [], item)) : value;
|
|
9412
|
+
}
|
|
9413
|
+
return isPlainRecord(value) ? withCalendarDays(fields ?? [], value) : value;
|
|
9414
|
+
}
|
|
9415
|
+
if (field.type === "block" && Array.isArray(value)) {
|
|
9416
|
+
return mapItems(value, (item) => blockItemWithDays(field, item));
|
|
9417
|
+
}
|
|
9418
|
+
return value;
|
|
9419
|
+
};
|
|
9420
|
+
entryAsRead = (fields, data) => withItemKeys(fields, withCalendarDays(fields, data), "content");
|
|
9353
9421
|
envFlag2 = (name) => typeof process !== "undefined" && process.env[name] === "true";
|
|
9354
9422
|
LOG_LEVELS2 = {
|
|
9355
9423
|
DEBUG: 0,
|
|
@@ -10317,7 +10385,6 @@ var init_generate_ai_content = __esm({
|
|
|
10317
10385
|
};
|
|
10318
10386
|
isDataOnlyFormat = (format) => format === "json" || format === "yaml";
|
|
10319
10387
|
asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
10320
|
-
init_item_keys();
|
|
10321
10388
|
DEFAULT_SEO_FIELD_NAMES = {
|
|
10322
10389
|
title: "metaTitle",
|
|
10323
10390
|
description: "metaDescription",
|
|
@@ -11624,7 +11691,7 @@ var init_generate_ai_content = __esm({
|
|
|
11624
11691
|
absolutePath
|
|
11625
11692
|
};
|
|
11626
11693
|
}
|
|
11627
|
-
doc.data =
|
|
11694
|
+
doc.data = entryAsRead(fields, doc.data);
|
|
11628
11695
|
if (options.resolveReferences !== false) {
|
|
11629
11696
|
doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess, {
|
|
11630
11697
|
entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
|
|
@@ -11728,7 +11795,7 @@ var init_generate_ai_content = __esm({
|
|
|
11728
11795
|
} else if (input.format === "yaml") {
|
|
11729
11796
|
content = serializeYaml(input.data ?? {}, existingRaw);
|
|
11730
11797
|
} else {
|
|
11731
|
-
content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
|
|
11798
|
+
content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
|
|
11732
11799
|
}
|
|
11733
11800
|
await atomicWriteFile2(absolutePath, content);
|
|
11734
11801
|
const liveIndex = this._idIndex;
|
|
@@ -12324,6 +12391,7 @@ var init_generate_ai_content = __esm({
|
|
|
12324
12391
|
FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
|
|
12325
12392
|
"string",
|
|
12326
12393
|
"number",
|
|
12394
|
+
"date",
|
|
12327
12395
|
"datetime",
|
|
12328
12396
|
"boolean",
|
|
12329
12397
|
"select",
|
|
@@ -14593,8 +14661,8 @@ var init_generate_ai_content = __esm({
|
|
|
14593
14661
|
return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
14594
14662
|
}
|
|
14595
14663
|
};
|
|
14664
|
+
AI_USER_ID = "canopycms-ai";
|
|
14596
14665
|
init_error3();
|
|
14597
|
-
init_item_keys();
|
|
14598
14666
|
log10 = createDebugLogger2({ prefix: "ContentListing" });
|
|
14599
14667
|
normalizePath = (root, target) => {
|
|
14600
14668
|
const result = validateAndNormalizePath(root, target);
|
|
@@ -14704,7 +14772,7 @@ var init_generate_ai_content = __esm({
|
|
|
14704
14772
|
fs20.stat(absolutePath),
|
|
14705
14773
|
readEntryData(absolutePath, format, bodyField)
|
|
14706
14774
|
]);
|
|
14707
|
-
const data =
|
|
14775
|
+
const data = entryAsRead(entryType?.schema ?? [], rawData);
|
|
14708
14776
|
const item = {
|
|
14709
14777
|
logicalPath: entryLogicalPath(collection.logicalPath, slug),
|
|
14710
14778
|
contentId,
|
|
@@ -16598,7 +16666,7 @@ var scalarFieldTypes3, markdownFieldTypes3, primitiveFieldTypes3, fieldTypes3;
|
|
|
16598
16666
|
var init_types3 = __esm({
|
|
16599
16667
|
"dist/config/types.js"() {
|
|
16600
16668
|
"use strict";
|
|
16601
|
-
scalarFieldTypes3 = ["string", "number", "boolean", "datetime", "code"];
|
|
16669
|
+
scalarFieldTypes3 = ["string", "number", "boolean", "date", "datetime", "code"];
|
|
16602
16670
|
markdownFieldTypes3 = ["markdown", "mdx"];
|
|
16603
16671
|
primitiveFieldTypes3 = [...scalarFieldTypes3, ...markdownFieldTypes3];
|
|
16604
16672
|
fieldTypes3 = [
|
|
@@ -17045,6 +17113,10 @@ function sanitizeBranchName3(branchName) {
|
|
|
17045
17113
|
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
17046
17114
|
return trimmedDots || "branch";
|
|
17047
17115
|
}
|
|
17116
|
+
function isSettingsBranchName2(branchName, settingsBranchName) {
|
|
17117
|
+
const sanitized = sanitizeBranchName3(branchName);
|
|
17118
|
+
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX3) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName3(settingsBranchName);
|
|
17119
|
+
}
|
|
17048
17120
|
var RESERVED_SETTINGS_BRANCH_PREFIX3;
|
|
17049
17121
|
var init_branch_name2 = __esm({
|
|
17050
17122
|
"dist/paths/branch-name.js"() {
|
|
@@ -17057,6 +17129,18 @@ var init_branch_name2 = __esm({
|
|
|
17057
17129
|
function isValidContentId2(id) {
|
|
17058
17130
|
return CONTENT_ID_PATTERN3.test(id);
|
|
17059
17131
|
}
|
|
17132
|
+
function isExactBranchForm(param) {
|
|
17133
|
+
return param.startsWith(EXACT_BRANCH_MARK);
|
|
17134
|
+
}
|
|
17135
|
+
function exactBranchSegment(fullName) {
|
|
17136
|
+
return `${EXACT_BRANCH_MARK}${fullName.replaceAll("/", EXACT_BRANCH_MARK)}`;
|
|
17137
|
+
}
|
|
17138
|
+
function exactBranchName(param) {
|
|
17139
|
+
if (!isExactBranchForm(param) || param.includes("/"))
|
|
17140
|
+
return null;
|
|
17141
|
+
const named = parseBranchRef3(param.slice(EXACT_BRANCH_MARK.length).replaceAll(EXACT_BRANCH_MARK, "/"));
|
|
17142
|
+
return named.ok ? named.name : null;
|
|
17143
|
+
}
|
|
17060
17144
|
function parseBranchRef3(name) {
|
|
17061
17145
|
if (!name || typeof name !== "string") {
|
|
17062
17146
|
return { ok: false, error: "Branch name is required" };
|
|
@@ -17124,7 +17208,7 @@ function parseSlug2(slug) {
|
|
|
17124
17208
|
}
|
|
17125
17209
|
return { ok: true, slug: normalized };
|
|
17126
17210
|
}
|
|
17127
|
-
var BASE58_PATTERN3, CONTENT_ID_PATTERN3, PHYSICAL_SEGMENT_PATTERN3, MAX_BRANCH_NAME_LENGTH3;
|
|
17211
|
+
var BASE58_PATTERN3, CONTENT_ID_PATTERN3, PHYSICAL_SEGMENT_PATTERN3, MAX_BRANCH_NAME_LENGTH3, EXACT_BRANCH_MARK;
|
|
17128
17212
|
var init_validation3 = __esm({
|
|
17129
17213
|
"dist/paths/validation.js"() {
|
|
17130
17214
|
"use strict";
|
|
@@ -17132,6 +17216,7 @@ var init_validation3 = __esm({
|
|
|
17132
17216
|
CONTENT_ID_PATTERN3 = new RegExp(`^${BASE58_PATTERN3}{12}$`);
|
|
17133
17217
|
PHYSICAL_SEGMENT_PATTERN3 = new RegExp(`\\.${BASE58_PATTERN3}{12}(?:\\.[a-z]+)?$`);
|
|
17134
17218
|
MAX_BRANCH_NAME_LENGTH3 = 250;
|
|
17219
|
+
EXACT_BRANCH_MARK = "~";
|
|
17135
17220
|
}
|
|
17136
17221
|
});
|
|
17137
17222
|
|
|
@@ -17508,27 +17593,39 @@ var init_operating_mode2 = __esm({
|
|
|
17508
17593
|
}
|
|
17509
17594
|
});
|
|
17510
17595
|
|
|
17511
|
-
// dist/cli/
|
|
17512
|
-
var configured_asset_store_exports = {};
|
|
17513
|
-
__export(configured_asset_store_exports, {
|
|
17514
|
-
loadConfiguredAssetStore: () => loadConfiguredAssetStore
|
|
17515
|
-
});
|
|
17596
|
+
// dist/cli/site-config.js
|
|
17516
17597
|
import path31 from "node:path";
|
|
17517
17598
|
import { createJiti as createJiti3 } from "jiti";
|
|
17518
|
-
|
|
17519
|
-
async function loadConfiguredAssetStore(projectDir) {
|
|
17599
|
+
async function importSiteServerConfig(projectDir) {
|
|
17520
17600
|
const configPath = path31.join(projectDir, "canopycms.config.ts");
|
|
17521
17601
|
const jiti2 = createJiti3(import.meta.url);
|
|
17522
17602
|
const mod = await jiti2.import(configPath);
|
|
17523
17603
|
const configExport = mod.default ?? mod.config ?? mod;
|
|
17524
17604
|
const server = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
17605
|
+
return { configPath, server };
|
|
17606
|
+
}
|
|
17607
|
+
var init_site_config = __esm({
|
|
17608
|
+
"dist/cli/site-config.js"() {
|
|
17609
|
+
"use strict";
|
|
17610
|
+
}
|
|
17611
|
+
});
|
|
17612
|
+
|
|
17613
|
+
// dist/cli/configured-asset-store.js
|
|
17614
|
+
var configured_asset_store_exports = {};
|
|
17615
|
+
__export(configured_asset_store_exports, {
|
|
17616
|
+
loadConfiguredAssetStore: () => loadConfiguredAssetStore
|
|
17617
|
+
});
|
|
17618
|
+
import path33 from "node:path";
|
|
17619
|
+
import { z as z15 } from "zod";
|
|
17620
|
+
async function loadConfiguredAssetStore(projectDir) {
|
|
17621
|
+
const { configPath, server } = await importSiteServerConfig(projectDir);
|
|
17525
17622
|
const parsed = serverConfigSchema.safeParse(server);
|
|
17526
17623
|
if (!parsed.success) {
|
|
17527
17624
|
throw new Error(`Invalid CanopyCMS config at ${configPath}: ${parsed.error.message}`);
|
|
17528
17625
|
}
|
|
17529
17626
|
const { mode, media } = parsed.data;
|
|
17530
|
-
const devAssetsDir = mode === "dev" ?
|
|
17531
|
-
const resolvedMedia = media?.adapter === "local" && media.directory ? { ...media, directory:
|
|
17627
|
+
const devAssetsDir = mode === "dev" ? path33.join(operatingStrategy3(mode).getWorkspaceRoot(projectDir), "assets") : void 0;
|
|
17628
|
+
const resolvedMedia = media?.adapter === "local" && media.directory ? { ...media, directory: path33.resolve(projectDir, media.directory) } : media;
|
|
17532
17629
|
const store = createAssetStore(resolvedMedia, { devAssetsDir });
|
|
17533
17630
|
if (!store) {
|
|
17534
17631
|
throw new Error(`No asset store is configured in ${configPath}: set \`media\` (an s3 adapter in prod).`);
|
|
@@ -17542,6 +17639,7 @@ var init_configured_asset_store = __esm({
|
|
|
17542
17639
|
init_factory();
|
|
17543
17640
|
init_media3();
|
|
17544
17641
|
init_operating_mode2();
|
|
17642
|
+
init_site_config();
|
|
17545
17643
|
serverConfigSchema = z15.object({
|
|
17546
17644
|
mode: z15.enum(["prod", "dev"]),
|
|
17547
17645
|
media: mediaSchema3.optional()
|
|
@@ -17557,13 +17655,13 @@ __export(asset_refs_exports, {
|
|
|
17557
17655
|
materializeAssetsCLI: () => materializeAssetsCLI
|
|
17558
17656
|
});
|
|
17559
17657
|
import fs27 from "node:fs/promises";
|
|
17560
|
-
import
|
|
17658
|
+
import path34 from "node:path";
|
|
17561
17659
|
async function collectAssetRefsCLI(options) {
|
|
17562
17660
|
if (!options.outDir) {
|
|
17563
17661
|
console.error("Usage: canopycms collect-asset-refs <outDir>");
|
|
17564
17662
|
return 1;
|
|
17565
17663
|
}
|
|
17566
|
-
const outDir =
|
|
17664
|
+
const outDir = path34.resolve(options.outDir);
|
|
17567
17665
|
try {
|
|
17568
17666
|
if (!(await fs27.stat(outDir)).isDirectory()) {
|
|
17569
17667
|
console.error(`Not a directory: ${outDir}`);
|
|
@@ -17658,19 +17756,19 @@ async function runMaterialize(options) {
|
|
|
17658
17756
|
console.error("canopycms materialize-assets: --report needs a file");
|
|
17659
17757
|
return MATERIALIZE_EXIT_CODES.error;
|
|
17660
17758
|
}
|
|
17661
|
-
if (options.reportPath &&
|
|
17759
|
+
if (options.reportPath && path34.resolve(options.reportPath) === path34.resolve(options.refsPath)) {
|
|
17662
17760
|
console.error("canopycms materialize-assets: --report must not be the --refs file");
|
|
17663
17761
|
return MATERIALIZE_EXIT_CODES.error;
|
|
17664
17762
|
}
|
|
17665
17763
|
if (options.reportPath)
|
|
17666
|
-
await fs27.rm(
|
|
17764
|
+
await fs27.rm(path34.resolve(options.reportPath), { force: true });
|
|
17667
17765
|
const concurrency = parsePositiveInteger("--concurrency", options.concurrency);
|
|
17668
17766
|
const transformConcurrency = parsePositiveInteger("--transform-concurrency", options.transformConcurrency);
|
|
17669
17767
|
if (concurrency === null || transformConcurrency === null)
|
|
17670
17768
|
return MATERIALIZE_EXIT_CODES.error;
|
|
17671
17769
|
if (options.outputPrefix !== void 0)
|
|
17672
17770
|
assertValidOutputPrefix(options.outputPrefix);
|
|
17673
|
-
const refs = await readAssetRefsFile(
|
|
17771
|
+
const refs = await readAssetRefsFile(path34.resolve(options.refsPath));
|
|
17674
17772
|
const store = await resolveStore(options);
|
|
17675
17773
|
if (!store)
|
|
17676
17774
|
return MATERIALIZE_EXIT_CODES.error;
|
|
@@ -17684,7 +17782,7 @@ async function runMaterialize(options) {
|
|
|
17684
17782
|
});
|
|
17685
17783
|
printReport(report);
|
|
17686
17784
|
if (options.reportPath) {
|
|
17687
|
-
await atomicWriteFile3(
|
|
17785
|
+
await atomicWriteFile3(path34.resolve(options.reportPath), `${JSON.stringify(report, null, 2)}
|
|
17688
17786
|
`);
|
|
17689
17787
|
}
|
|
17690
17788
|
const { contentFailures, storeFailures } = report.summary;
|
|
@@ -17724,20 +17822,149 @@ var init_asset_refs2 = __esm({
|
|
|
17724
17822
|
}
|
|
17725
17823
|
});
|
|
17726
17824
|
|
|
17727
|
-
// dist/
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17825
|
+
// dist/branch-metadata-error.js
|
|
17826
|
+
var BranchMetadataCorruptError2;
|
|
17827
|
+
var init_branch_metadata_error = __esm({
|
|
17828
|
+
"dist/branch-metadata-error.js"() {
|
|
17829
|
+
"use strict";
|
|
17830
|
+
BranchMetadataCorruptError2 = class extends Error {
|
|
17831
|
+
constructor(branchRoot, cause) {
|
|
17832
|
+
super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
|
|
17833
|
+
this.name = "BranchMetadataCorruptError";
|
|
17834
|
+
this.branchRoot = branchRoot;
|
|
17835
|
+
this.parseCause = cause;
|
|
17836
|
+
}
|
|
17837
|
+
};
|
|
17838
|
+
}
|
|
17839
|
+
});
|
|
17840
|
+
|
|
17841
|
+
// dist/branch-metadata-file.js
|
|
17842
|
+
import fs28 from "node:fs/promises";
|
|
17843
|
+
import path35 from "node:path";
|
|
17844
|
+
import { z as z16 } from "zod";
|
|
17845
|
+
function describeSchemaIssues2(issues) {
|
|
17846
|
+
const missing = /* @__PURE__ */ new Set();
|
|
17847
|
+
const invalid = /* @__PURE__ */ new Set();
|
|
17848
|
+
for (const issue of issues) {
|
|
17849
|
+
const field = issue.path.length > 0 ? issue.path.join(".") : "the file";
|
|
17850
|
+
const isMissing = issue.code === z16.ZodIssueCode.invalid_type && issue.received === z16.ZodParsedType.undefined;
|
|
17851
|
+
(isMissing ? missing : invalid).add(field);
|
|
17852
|
+
}
|
|
17853
|
+
const parts = [];
|
|
17854
|
+
if (missing.size > 0)
|
|
17855
|
+
parts.push(`Missing: ${[...missing].join(", ")}`);
|
|
17856
|
+
if (invalid.size > 0)
|
|
17857
|
+
parts.push(`Invalid: ${[...invalid].join(", ")}`);
|
|
17858
|
+
return `Not branch metadata. ${parts.join(". ")}`;
|
|
17731
17859
|
}
|
|
17732
|
-
async function
|
|
17733
|
-
|
|
17734
|
-
|
|
17860
|
+
async function readBranchMetadataFile2(branchRoot) {
|
|
17861
|
+
const resolvedRoot = path35.resolve(branchRoot);
|
|
17862
|
+
let raw;
|
|
17863
|
+
try {
|
|
17864
|
+
raw = await fs28.readFile(branchMetadataFilePath2(resolvedRoot), "utf8");
|
|
17865
|
+
} catch (err2) {
|
|
17866
|
+
if (isNotFoundError(err2)) {
|
|
17867
|
+
return null;
|
|
17868
|
+
}
|
|
17869
|
+
throw err2;
|
|
17870
|
+
}
|
|
17871
|
+
return parseBranchMetadataFile2(resolvedRoot, raw);
|
|
17735
17872
|
}
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17873
|
+
function parseBranchMetadataFile2(branchRoot, raw) {
|
|
17874
|
+
let json;
|
|
17875
|
+
try {
|
|
17876
|
+
json = JSON.parse(raw);
|
|
17877
|
+
} catch (err2) {
|
|
17878
|
+
throw new BranchMetadataCorruptError2(branchRoot, `Not valid JSON: ${getErrorMessage(err2)}`);
|
|
17879
|
+
}
|
|
17880
|
+
const result = branchMetadataFileSchema2.safeParse(json);
|
|
17881
|
+
if (!result.success) {
|
|
17882
|
+
throw new BranchMetadataCorruptError2(branchRoot, describeSchemaIssues2(result.error.issues));
|
|
17883
|
+
}
|
|
17884
|
+
const file = result.data;
|
|
17885
|
+
return file;
|
|
17886
|
+
}
|
|
17887
|
+
var BRANCH_META_DIR2, BRANCH_META_FILE2, branchMetadataFilePath2, UNKNOWN_TIME2, enumOf2, branchMetadataShape2, branchMetadataSchema2, branchMetadataFileSchema2;
|
|
17888
|
+
var init_branch_metadata_file = __esm({
|
|
17889
|
+
"dist/branch-metadata-file.js"() {
|
|
17739
17890
|
"use strict";
|
|
17740
|
-
|
|
17891
|
+
init_error();
|
|
17892
|
+
init_branch_metadata_error();
|
|
17893
|
+
BRANCH_META_DIR2 = ".canopy-meta";
|
|
17894
|
+
BRANCH_META_FILE2 = "branch.json";
|
|
17895
|
+
branchMetadataFilePath2 = (branchRoot) => path35.join(path35.resolve(branchRoot), BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
17896
|
+
UNKNOWN_TIME2 = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
17897
|
+
enumOf2 = (values) => z16.nativeEnum(values);
|
|
17898
|
+
branchMetadataShape2 = {
|
|
17899
|
+
name: z16.string().min(1),
|
|
17900
|
+
title: z16.string().optional(),
|
|
17901
|
+
description: z16.string().optional(),
|
|
17902
|
+
status: enumOf2({
|
|
17903
|
+
editing: "editing",
|
|
17904
|
+
submitted: "submitted",
|
|
17905
|
+
approved: "approved",
|
|
17906
|
+
archived: "archived"
|
|
17907
|
+
}),
|
|
17908
|
+
access: z16.object({
|
|
17909
|
+
allowedUsers: z16.array(z16.string()).optional(),
|
|
17910
|
+
allowedGroups: z16.array(z16.string()).optional(),
|
|
17911
|
+
managerOrAdminAllowed: z16.boolean().optional()
|
|
17912
|
+
}).passthrough(),
|
|
17913
|
+
createdBy: z16.string().default("unknown"),
|
|
17914
|
+
createdAt: z16.string().default(UNKNOWN_TIME2),
|
|
17915
|
+
updatedAt: z16.string().default(UNKNOWN_TIME2),
|
|
17916
|
+
baseBranch: z16.string().optional(),
|
|
17917
|
+
pullRequestUrl: z16.string().optional(),
|
|
17918
|
+
pullRequestNumber: z16.number().int().optional(),
|
|
17919
|
+
submittedAt: z16.string().optional(),
|
|
17920
|
+
pushedToGitHubAt: z16.string().optional(),
|
|
17921
|
+
editorUrl: z16.string().optional(),
|
|
17922
|
+
syncStatus: enumOf2({
|
|
17923
|
+
synced: "synced",
|
|
17924
|
+
"pending-sync": "pending-sync",
|
|
17925
|
+
"sync-failed": "sync-failed"
|
|
17926
|
+
}).optional(),
|
|
17927
|
+
conflictStatus: enumOf2({
|
|
17928
|
+
clean: "clean",
|
|
17929
|
+
"conflicts-detected": "conflicts-detected"
|
|
17930
|
+
}).optional(),
|
|
17931
|
+
conflictFiles: z16.array(z16.custom((v) => typeof v === "string")).optional(),
|
|
17932
|
+
pullRequestState: enumOf2({
|
|
17933
|
+
open: "open",
|
|
17934
|
+
closed: "closed",
|
|
17935
|
+
merged: "merged"
|
|
17936
|
+
}).optional(),
|
|
17937
|
+
mergedAt: z16.string().optional(),
|
|
17938
|
+
rebaseFailure: z16.object({ message: z16.string(), firstAt: z16.string(), lastAt: z16.string() }).optional(),
|
|
17939
|
+
historyRewrittenFrom: z16.string().optional(),
|
|
17940
|
+
lastGitHubTip: z16.string().optional(),
|
|
17941
|
+
keptOverGitHub: z16.array(z16.string()).optional(),
|
|
17942
|
+
rewrittenOnGitHub: z16.string().optional(),
|
|
17943
|
+
submitUnfinished: z16.boolean().optional(),
|
|
17944
|
+
submitNotQueued: z16.boolean().optional(),
|
|
17945
|
+
unpublishedAtMerge: z16.boolean().optional(),
|
|
17946
|
+
syncFailureReason: z16.string().optional(),
|
|
17947
|
+
editors: z16.array(z16.string()).optional(),
|
|
17948
|
+
uncommittedEditors: z16.array(z16.string()).optional(),
|
|
17949
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
17950
|
+
pullRequestHold: z16.discriminatedUnion("kind", [
|
|
17951
|
+
z16.object({ kind: z16.literal("conflicts") }),
|
|
17952
|
+
z16.object({ kind: z16.literal("retargeted"), base: z16.string() })
|
|
17953
|
+
]).optional().catch(void 0),
|
|
17954
|
+
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
17955
|
+
review: z16.object({
|
|
17956
|
+
state: z16.enum(["approved", "changes-requested"]),
|
|
17957
|
+
by: z16.object({ userId: z16.string(), name: z16.string().optional() }),
|
|
17958
|
+
at: z16.string()
|
|
17959
|
+
}).optional().catch(void 0)
|
|
17960
|
+
};
|
|
17961
|
+
branchMetadataSchema2 = z16.object(branchMetadataShape2).passthrough();
|
|
17962
|
+
branchMetadataFileSchema2 = z16.object({
|
|
17963
|
+
schemaVersion: z16.number().int().default(1),
|
|
17964
|
+
version: z16.number().int().nonnegative().default(0),
|
|
17965
|
+
writeId: z16.string().optional(),
|
|
17966
|
+
branch: branchMetadataSchema2
|
|
17967
|
+
}).passthrough();
|
|
17741
17968
|
}
|
|
17742
17969
|
});
|
|
17743
17970
|
|
|
@@ -17846,11 +18073,11 @@ var init_debug2 = __esm({
|
|
|
17846
18073
|
});
|
|
17847
18074
|
|
|
17848
18075
|
// dist/resource-generation.js
|
|
17849
|
-
import
|
|
17850
|
-
import
|
|
18076
|
+
import fs29 from "node:fs/promises";
|
|
18077
|
+
import path36 from "node:path";
|
|
17851
18078
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
17852
18079
|
function resourceGenerationPath2(root, resource) {
|
|
17853
|
-
return
|
|
18080
|
+
return path36.resolve(root, META_DIR3, `${resource}.generation`);
|
|
17854
18081
|
}
|
|
17855
18082
|
async function bumpResourceGeneration2(root, resource, options) {
|
|
17856
18083
|
const token = randomUUID5();
|
|
@@ -17866,7 +18093,7 @@ async function bumpResourceGeneration2(root, resource, options) {
|
|
|
17866
18093
|
}
|
|
17867
18094
|
async function readResourceGeneration2(root, resource) {
|
|
17868
18095
|
try {
|
|
17869
|
-
const token = await
|
|
18096
|
+
const token = await fs29.readFile(resourceGenerationPath2(root, resource), "utf-8");
|
|
17870
18097
|
return { ok: true, token };
|
|
17871
18098
|
} catch (err2) {
|
|
17872
18099
|
if (isNodeError(err2) && err2.code === "ENOENT")
|
|
@@ -17893,270 +18120,314 @@ var init_resource_generation = __esm({
|
|
|
17893
18120
|
}
|
|
17894
18121
|
});
|
|
17895
18122
|
|
|
17896
|
-
// dist/
|
|
17897
|
-
import
|
|
17898
|
-
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
continue;
|
|
17904
|
-
for (const ref of refs) {
|
|
17905
|
-
const target = ref.deref();
|
|
17906
|
-
if (target) {
|
|
17907
|
-
target.invalidateIndex();
|
|
17908
|
-
} else {
|
|
17909
|
-
refs.delete(ref);
|
|
17910
|
-
}
|
|
17911
|
-
}
|
|
17912
|
-
if (refs.size === 0)
|
|
17913
|
-
registry2.delete(storeRoot);
|
|
17914
|
-
}
|
|
17915
|
-
}
|
|
17916
|
-
var registry2, finalization2;
|
|
17917
|
-
var init_content_index_registry = __esm({
|
|
17918
|
-
"dist/content-index-registry.js"() {
|
|
18123
|
+
// dist/branch-registry.js
|
|
18124
|
+
import fs30 from "node:fs/promises";
|
|
18125
|
+
import path37 from "node:path";
|
|
18126
|
+
import { z as z17 } from "zod";
|
|
18127
|
+
var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE3, GET_MISS_REFRESH_MIN_INTERVAL_MS2, snapshotSchema2, reportedUnreadable2, BranchRegistry2;
|
|
18128
|
+
var init_branch_registry = __esm({
|
|
18129
|
+
"dist/branch-registry.js"() {
|
|
17919
18130
|
"use strict";
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
18131
|
+
init_branch_metadata_file();
|
|
18132
|
+
init_error();
|
|
18133
|
+
init_debug2();
|
|
18134
|
+
init_logger3();
|
|
18135
|
+
init_resource_generation();
|
|
18136
|
+
log12 = createDebugLogger3({ prefix: "BranchRegistry" });
|
|
18137
|
+
REGISTRY_FILE2 = "branches.json";
|
|
18138
|
+
REGISTRY_STALE_FILE2 = "branches.stale.json";
|
|
18139
|
+
REGISTRY_TEMP_FILE2 = "branches.tmp.json";
|
|
18140
|
+
REGISTRY_VERSION2 = 2;
|
|
18141
|
+
RESOURCE3 = "branch-registry";
|
|
18142
|
+
GET_MISS_REFRESH_MIN_INTERVAL_MS2 = 5e3;
|
|
18143
|
+
snapshotSchema2 = z17.object({
|
|
18144
|
+
version: z17.literal(REGISTRY_VERSION2),
|
|
18145
|
+
branches: z17.array(z17.object({ branch: branchMetadataSchema2, branchRoot: z17.string(), baseRoot: z17.string() })),
|
|
18146
|
+
generation: z17.string().nullable()
|
|
17928
18147
|
});
|
|
18148
|
+
reportedUnreadable2 = /* @__PURE__ */ new Set();
|
|
18149
|
+
BranchRegistry2 = class {
|
|
18150
|
+
constructor(root) {
|
|
18151
|
+
this.regenInFlight = null;
|
|
18152
|
+
this.lastForcedRefreshMs = 0;
|
|
18153
|
+
this.root = path37.resolve(root);
|
|
18154
|
+
this.registryPath = path37.join(this.root, REGISTRY_FILE2);
|
|
18155
|
+
this.stalePath = path37.join(this.root, REGISTRY_STALE_FILE2);
|
|
18156
|
+
this.tempPath = path37.join(this.root, REGISTRY_TEMP_FILE2);
|
|
18157
|
+
}
|
|
18158
|
+
/**
|
|
18159
|
+
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
18160
|
+
* token matches the live marker, regenerates otherwise.
|
|
18161
|
+
*
|
|
18162
|
+
* An unparseable or wrong-shaped snapshot is treated like an absent one: the
|
|
18163
|
+
* snapshot is a cache of the branch directories, so rebuilding it loses
|
|
18164
|
+
* nothing, while rethrowing would fail every editor's branch listing until
|
|
18165
|
+
* someone deleted the file. Other read failures (permissions, IO) propagate.
|
|
18166
|
+
*/
|
|
18167
|
+
async list() {
|
|
18168
|
+
let raw;
|
|
18169
|
+
try {
|
|
18170
|
+
raw = await fs30.readFile(this.registryPath, "utf8");
|
|
18171
|
+
} catch (err2) {
|
|
18172
|
+
if (isNotFoundError(err2)) {
|
|
18173
|
+
return await this.regenerate();
|
|
18174
|
+
}
|
|
18175
|
+
throw err2;
|
|
18176
|
+
}
|
|
18177
|
+
const parsed = this.parseSnapshot(raw);
|
|
18178
|
+
if (!parsed)
|
|
18179
|
+
return await this.regenerate();
|
|
18180
|
+
const read = await readResourceGeneration2(this.root, RESOURCE3);
|
|
18181
|
+
if (isGenerationCurrent2(parsed.generation, read)) {
|
|
18182
|
+
return parsed.branches;
|
|
18183
|
+
}
|
|
18184
|
+
return await this.regenerate();
|
|
18185
|
+
}
|
|
18186
|
+
/** The snapshot in `raw`, or null when it must be rebuilt. */
|
|
18187
|
+
parseSnapshot(raw) {
|
|
18188
|
+
let json;
|
|
18189
|
+
try {
|
|
18190
|
+
json = JSON.parse(raw);
|
|
18191
|
+
} catch (err2) {
|
|
18192
|
+
this.reportUnreadable(getErrorMessage(err2));
|
|
18193
|
+
return null;
|
|
18194
|
+
}
|
|
18195
|
+
if (typeof json === "object" && json !== null && "version" in json && typeof json.version === "number" && json.version !== REGISTRY_VERSION2) {
|
|
18196
|
+
return null;
|
|
18197
|
+
}
|
|
18198
|
+
const result = snapshotSchema2.safeParse(json);
|
|
18199
|
+
if (!result.success) {
|
|
18200
|
+
this.reportUnreadable("it does not have the registry snapshot shape");
|
|
18201
|
+
return null;
|
|
18202
|
+
}
|
|
18203
|
+
reportedUnreadable2.delete(this.registryPath);
|
|
18204
|
+
const snapshot = result.data;
|
|
18205
|
+
return snapshot;
|
|
18206
|
+
}
|
|
18207
|
+
reportUnreadable(reason) {
|
|
18208
|
+
if (reportedUnreadable2.has(this.registryPath))
|
|
18209
|
+
return;
|
|
18210
|
+
reportedUnreadable2.add(this.registryPath);
|
|
18211
|
+
canopyLogWarn3(`CanopyCMS: Branch registry snapshot ${this.registryPath} is unreadable (${reason}); regenerating it from the branch directories`);
|
|
18212
|
+
}
|
|
18213
|
+
/**
|
|
18214
|
+
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
18215
|
+
*
|
|
18216
|
+
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
18217
|
+
* the snapshot may predate a branch that now exists, so force one fresh
|
|
18218
|
+
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
18219
|
+
* genuinely-absent branch costs at most one extra scan per window.
|
|
18220
|
+
*/
|
|
18221
|
+
async get(name) {
|
|
18222
|
+
return this.find((b) => b.branch.name === name);
|
|
18223
|
+
}
|
|
18224
|
+
/**
|
|
18225
|
+
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
18226
|
+
* whatever its name, with {@link get}'s backstop.
|
|
18227
|
+
*/
|
|
18228
|
+
async getByKey(key) {
|
|
18229
|
+
return this.find((b) => path37.basename(b.branchRoot) === key);
|
|
18230
|
+
}
|
|
18231
|
+
async find(matches) {
|
|
18232
|
+
const found = (await this.list()).find(matches);
|
|
18233
|
+
if (found)
|
|
18234
|
+
return found;
|
|
18235
|
+
if (!this.shouldForceRefresh())
|
|
18236
|
+
return void 0;
|
|
18237
|
+
const refreshed = await this.regenerate();
|
|
18238
|
+
return refreshed.find(matches);
|
|
18239
|
+
}
|
|
18240
|
+
shouldForceRefresh() {
|
|
18241
|
+
const now = Date.now();
|
|
18242
|
+
if (now - this.lastForcedRefreshMs < GET_MISS_REFRESH_MIN_INTERVAL_MS2)
|
|
18243
|
+
return false;
|
|
18244
|
+
this.lastForcedRefreshMs = now;
|
|
18245
|
+
return true;
|
|
18246
|
+
}
|
|
18247
|
+
/**
|
|
18248
|
+
* Marks the cache stale for every process sharing this root by bumping the
|
|
18249
|
+
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
18250
|
+
* swallowing that failure leaves the registry stale indefinitely with no
|
|
18251
|
+
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
18252
|
+
* bump alone already restored correctness for every future reader.
|
|
18253
|
+
*/
|
|
18254
|
+
async invalidate() {
|
|
18255
|
+
await bumpResourceGeneration2(this.root, RESOURCE3, { mustSucceed: true });
|
|
18256
|
+
try {
|
|
18257
|
+
if (this.regenInFlight)
|
|
18258
|
+
await this.regenInFlight.catch(() => {
|
|
18259
|
+
});
|
|
18260
|
+
await this.regenerate();
|
|
18261
|
+
} catch (err2) {
|
|
18262
|
+
log12.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
18263
|
+
error: getErrorMessage(err2)
|
|
18264
|
+
});
|
|
18265
|
+
}
|
|
18266
|
+
}
|
|
18267
|
+
/**
|
|
18268
|
+
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
18269
|
+
* callers on this instance to a single underlying scan. Never loops waiting
|
|
18270
|
+
* for the embedded token to match the marker — that livelocks under a bump
|
|
18271
|
+
* storm; a caller wanting the very latest state calls list() again.
|
|
18272
|
+
*/
|
|
18273
|
+
async regenerate() {
|
|
18274
|
+
if (this.regenInFlight)
|
|
18275
|
+
return this.regenInFlight;
|
|
18276
|
+
const run = this.performRegenerate();
|
|
18277
|
+
this.regenInFlight = run;
|
|
18278
|
+
try {
|
|
18279
|
+
return await run;
|
|
18280
|
+
} finally {
|
|
18281
|
+
this.regenInFlight = null;
|
|
18282
|
+
}
|
|
18283
|
+
}
|
|
18284
|
+
async performRegenerate() {
|
|
18285
|
+
const read = await readResourceGeneration2(this.root, RESOURCE3);
|
|
18286
|
+
const branches = await this.scanBranchDirectories();
|
|
18287
|
+
await fs30.unlink(this.stalePath).catch(() => {
|
|
18288
|
+
});
|
|
18289
|
+
if (!read.ok) {
|
|
18290
|
+
return branches;
|
|
18291
|
+
}
|
|
18292
|
+
const snapshot = {
|
|
18293
|
+
version: REGISTRY_VERSION2,
|
|
18294
|
+
branches,
|
|
18295
|
+
generation: read.token
|
|
18296
|
+
};
|
|
18297
|
+
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
18298
|
+
await fs30.mkdir(this.root, { recursive: true });
|
|
18299
|
+
await fs30.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
18300
|
+
try {
|
|
18301
|
+
await fs30.rename(uniqueTempPath, this.registryPath);
|
|
18302
|
+
} catch (err2) {
|
|
18303
|
+
await fs30.unlink(uniqueTempPath).catch(() => {
|
|
18304
|
+
});
|
|
18305
|
+
throw err2;
|
|
18306
|
+
}
|
|
18307
|
+
return branches;
|
|
18308
|
+
}
|
|
18309
|
+
/**
|
|
18310
|
+
* Scans the root directory for branch subdirectories with valid branch.json files.
|
|
18311
|
+
* Protected (rather than exported as a test hook) so tests can subclass and
|
|
18312
|
+
* override to simulate cross-process interleavings.
|
|
18313
|
+
*/
|
|
18314
|
+
async scanBranchDirectories() {
|
|
18315
|
+
const branches = [];
|
|
18316
|
+
try {
|
|
18317
|
+
const entries = await fs30.readdir(this.root, { withFileTypes: true });
|
|
18318
|
+
for (const entry of entries) {
|
|
18319
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
18320
|
+
continue;
|
|
18321
|
+
}
|
|
18322
|
+
const branchRoot = path37.join(this.root, entry.name);
|
|
18323
|
+
let meta;
|
|
18324
|
+
try {
|
|
18325
|
+
meta = await readBranchMetadataFile2(branchRoot);
|
|
18326
|
+
} catch (err2) {
|
|
18327
|
+
canopyLogWarn3(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err2)}`);
|
|
18328
|
+
continue;
|
|
18329
|
+
}
|
|
18330
|
+
if (meta) {
|
|
18331
|
+
branches.push({
|
|
18332
|
+
branch: meta.branch,
|
|
18333
|
+
branchRoot,
|
|
18334
|
+
baseRoot: this.root
|
|
18335
|
+
});
|
|
18336
|
+
}
|
|
18337
|
+
}
|
|
18338
|
+
} catch (err2) {
|
|
18339
|
+
if (isNotFoundError(err2)) {
|
|
18340
|
+
return [];
|
|
18341
|
+
}
|
|
18342
|
+
throw err2;
|
|
18343
|
+
}
|
|
18344
|
+
return branches;
|
|
18345
|
+
}
|
|
18346
|
+
};
|
|
17929
18347
|
}
|
|
17930
|
-
});
|
|
17931
|
-
|
|
17932
|
-
// dist/
|
|
17933
|
-
import
|
|
17934
|
-
function
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
var isValidId2;
|
|
17939
|
-
var init_id = __esm({
|
|
17940
|
-
"dist/id.js"() {
|
|
17941
|
-
"use strict";
|
|
17942
|
-
init_validation3();
|
|
17943
|
-
isValidId2 = isValidContentId2;
|
|
17944
|
-
}
|
|
17945
|
-
});
|
|
17946
|
-
|
|
17947
|
-
// dist/utils/format.js
|
|
17948
|
-
var getFormatExtension2;
|
|
17949
|
-
var init_format = __esm({
|
|
17950
|
-
"dist/utils/format.js"() {
|
|
17951
|
-
"use strict";
|
|
17952
|
-
getFormatExtension2 = (format) => {
|
|
17953
|
-
if (format === "md")
|
|
17954
|
-
return ".md";
|
|
17955
|
-
if (format === "mdx")
|
|
17956
|
-
return ".mdx";
|
|
17957
|
-
if (format === "yaml")
|
|
17958
|
-
return ".yaml";
|
|
17959
|
-
return ".json";
|
|
17960
|
-
};
|
|
17961
|
-
}
|
|
17962
|
-
});
|
|
17963
|
-
|
|
17964
|
-
// dist/content-id-index.js
|
|
17965
|
-
function extractIdFromFilename2(filename) {
|
|
17966
|
-
if (filename.startsWith(".")) {
|
|
17967
|
-
return null;
|
|
17968
|
-
}
|
|
17969
|
-
const parts = filename.split(".");
|
|
17970
|
-
if (parts.length >= 3) {
|
|
17971
|
-
const candidate = parts[parts.length - 2];
|
|
17972
|
-
if (isValidId2(candidate))
|
|
17973
|
-
return candidate;
|
|
17974
|
-
}
|
|
17975
|
-
if (parts.length === 2) {
|
|
17976
|
-
const candidate = parts[parts.length - 1];
|
|
17977
|
-
if (isValidId2(candidate))
|
|
17978
|
-
return candidate;
|
|
17979
|
-
}
|
|
17980
|
-
return null;
|
|
18348
|
+
});
|
|
18349
|
+
|
|
18350
|
+
// dist/paths/branch.js
|
|
18351
|
+
import path38 from "node:path";
|
|
18352
|
+
function namesNoWorkspace(err2) {
|
|
18353
|
+
if (err2 instanceof BranchPathError2)
|
|
18354
|
+
return true;
|
|
18355
|
+
return isNodeError(err2) && (err2.code === "ENAMETOOLONG" || err2.code === "ENOTDIR");
|
|
17981
18356
|
}
|
|
17982
|
-
function
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
const possibleId = parts[parts.length - 2];
|
|
17986
|
-
if (isValidId2(possibleId)) {
|
|
17987
|
-
let slugParts = parts.slice(0, parts.length - 2);
|
|
17988
|
-
if (entryTypeName && slugParts.length > 1 && slugParts[0] === entryTypeName) {
|
|
17989
|
-
slugParts = slugParts.slice(1);
|
|
17990
|
-
} else if (parts.length >= 4 && slugParts.length > 1) {
|
|
17991
|
-
slugParts = slugParts.slice(1);
|
|
17992
|
-
}
|
|
17993
|
-
return slugParts.join(".").toLowerCase();
|
|
17994
|
-
}
|
|
18357
|
+
function resolveBranchPath2(options) {
|
|
18358
|
+
if (options.branchName.includes("..")) {
|
|
18359
|
+
throw new BranchPathError2("Branch name cannot contain traversal segments");
|
|
17995
18360
|
}
|
|
17996
|
-
if (
|
|
17997
|
-
|
|
17998
|
-
if (isValidId2(possibleId)) {
|
|
17999
|
-
return parts[0].toLowerCase();
|
|
18000
|
-
}
|
|
18361
|
+
if (isSettingsBranchName2(options.branchName)) {
|
|
18362
|
+
throw new BranchPathError2("Settings branches are not content branches");
|
|
18001
18363
|
}
|
|
18002
|
-
|
|
18003
|
-
|
|
18364
|
+
const safeBranch = sanitizeBranchName3(options.branchName);
|
|
18365
|
+
const strategy = operatingStrategy3(options.mode);
|
|
18366
|
+
const baseRoot = resolveContentBranchesRoot2(options.mode, options.basePathOverride);
|
|
18367
|
+
const normalizedBase = path38.resolve(baseRoot);
|
|
18368
|
+
const baseWithSep = normalizedBase.endsWith(path38.sep) ? normalizedBase : `${normalizedBase}${path38.sep}`;
|
|
18369
|
+
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
18370
|
+
const withinBase = (target) => {
|
|
18371
|
+
const resolved = path38.resolve(target);
|
|
18372
|
+
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
18373
|
+
};
|
|
18374
|
+
if (!withinBase(branchRoot)) {
|
|
18375
|
+
throw new BranchPathError2("Branch path resolves outside the base root");
|
|
18004
18376
|
}
|
|
18005
|
-
return
|
|
18377
|
+
return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
|
|
18006
18378
|
}
|
|
18007
|
-
var
|
|
18008
|
-
var
|
|
18009
|
-
"dist/
|
|
18379
|
+
var BranchPathError2, resolveContentBranchesRoot2;
|
|
18380
|
+
var init_branch = __esm({
|
|
18381
|
+
"dist/paths/branch.js"() {
|
|
18010
18382
|
"use strict";
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18383
|
+
init_operating_mode2();
|
|
18384
|
+
init_error();
|
|
18385
|
+
init_branch_name2();
|
|
18386
|
+
BranchPathError2 = class extends Error {
|
|
18387
|
+
};
|
|
18388
|
+
resolveContentBranchesRoot2 = (mode, override) => {
|
|
18389
|
+
return operatingStrategy3(mode).getContentBranchesRoot(override);
|
|
18390
|
+
};
|
|
18014
18391
|
}
|
|
18015
18392
|
});
|
|
18016
18393
|
|
|
18017
|
-
// dist/
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
import { z as z16 } from "zod";
|
|
18021
|
-
import chokidar2 from "chokidar";
|
|
18022
|
-
function stripEmbeddedIdFromName2(name) {
|
|
18023
|
-
return extractSlugFromFilename2(name);
|
|
18024
|
-
}
|
|
18025
|
-
async function scanForCollectionMeta2(baseDir, relativePath = "") {
|
|
18026
|
-
const collections = [];
|
|
18027
|
-
try {
|
|
18028
|
-
const entries = await fs29.readdir(baseDir, { withFileTypes: true });
|
|
18029
|
-
for (const entry of entries) {
|
|
18030
|
-
if (!entry.isDirectory())
|
|
18031
|
-
continue;
|
|
18032
|
-
const folderName = entry.name;
|
|
18033
|
-
const logicalName = stripEmbeddedIdFromName2(folderName);
|
|
18034
|
-
const collectionContentId = extractIdFromFilename2(folderName) ?? void 0;
|
|
18035
|
-
const folderPath = relativePath ? `${relativePath}/${logicalName}` : logicalName;
|
|
18036
|
-
const absolutePath = join3(baseDir, folderName);
|
|
18037
|
-
const metaPath = join3(absolutePath, ".collection.json");
|
|
18038
|
-
try {
|
|
18039
|
-
await fs29.access(metaPath);
|
|
18040
|
-
const content = await fs29.readFile(metaPath, "utf-8");
|
|
18041
|
-
const parsed = JSON.parse(content);
|
|
18042
|
-
const meta = collectionMetaSchema2.parse(parsed);
|
|
18043
|
-
collections.push({
|
|
18044
|
-
...meta,
|
|
18045
|
-
path: folderPath,
|
|
18046
|
-
contentId: collectionContentId
|
|
18047
|
-
});
|
|
18048
|
-
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
18049
|
-
collections.push(...nestedCollections);
|
|
18050
|
-
} catch (err2) {
|
|
18051
|
-
if (err2.code !== "ENOENT") {
|
|
18052
|
-
canopyLogError2(`Error loading ${metaPath}:`, err2);
|
|
18053
|
-
throw new Error(`Invalid .collection.json in ${folderPath}: ${err2.message}`);
|
|
18054
|
-
}
|
|
18055
|
-
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
18056
|
-
collections.push(...nestedCollections);
|
|
18057
|
-
}
|
|
18058
|
-
}
|
|
18059
|
-
return collections;
|
|
18060
|
-
} catch (err2) {
|
|
18061
|
-
if (err2.code === "ENOENT") {
|
|
18062
|
-
return [];
|
|
18063
|
-
}
|
|
18064
|
-
throw err2;
|
|
18065
|
-
}
|
|
18066
|
-
}
|
|
18067
|
-
async function loadCollectionMetaFiles2(contentRoot) {
|
|
18068
|
-
let root = null;
|
|
18069
|
-
const rootMetaPath = join3(contentRoot, ".collection.json");
|
|
18070
|
-
try {
|
|
18071
|
-
await fs29.access(rootMetaPath);
|
|
18072
|
-
} catch (err2) {
|
|
18073
|
-
if (err2.code === "ENOENT") {
|
|
18074
|
-
} else {
|
|
18075
|
-
throw err2;
|
|
18076
|
-
}
|
|
18077
|
-
}
|
|
18078
|
-
try {
|
|
18079
|
-
const content = await fs29.readFile(rootMetaPath, "utf-8");
|
|
18080
|
-
const parsed = JSON.parse(content);
|
|
18081
|
-
root = rootCollectionMetaSchema2.parse(parsed);
|
|
18082
|
-
} catch (err2) {
|
|
18083
|
-
const errno = err2.code;
|
|
18084
|
-
if (errno !== "ENOENT") {
|
|
18085
|
-
throw new Error(`Invalid root .collection.json`);
|
|
18086
|
-
}
|
|
18087
|
-
}
|
|
18088
|
-
const collections = await scanForCollectionMeta2(contentRoot);
|
|
18089
|
-
return { root, collections };
|
|
18090
|
-
}
|
|
18091
|
-
var entryTypeMetaSchema2, collectionMetaSchema2, rootCollectionMetaSchema2;
|
|
18092
|
-
var init_meta_loader = __esm({
|
|
18093
|
-
"dist/schema/meta-loader.js"() {
|
|
18394
|
+
// dist/paths/index.js
|
|
18395
|
+
var init_paths = __esm({
|
|
18396
|
+
"dist/paths/index.js"() {
|
|
18094
18397
|
"use strict";
|
|
18095
|
-
|
|
18096
|
-
|
|
18097
|
-
entryTypeMetaSchema2 = z16.object({
|
|
18098
|
-
name: z16.string().min(1),
|
|
18099
|
-
format: z16.enum(["md", "mdx", "json", "yaml"]),
|
|
18100
|
-
schema: z16.string().min(1),
|
|
18101
|
-
// Entry schema registry key (validated at resolution time)
|
|
18102
|
-
label: z16.string().optional(),
|
|
18103
|
-
description: z16.string().optional(),
|
|
18104
|
-
default: z16.boolean().optional(),
|
|
18105
|
-
maxItems: z16.number().int().positive().optional()
|
|
18106
|
-
});
|
|
18107
|
-
collectionMetaSchema2 = z16.object({
|
|
18108
|
-
name: z16.string().min(1),
|
|
18109
|
-
label: z16.string().optional(),
|
|
18110
|
-
description: z16.string().optional(),
|
|
18111
|
-
entries: z16.array(entryTypeMetaSchema2).optional(),
|
|
18112
|
-
order: z16.array(z16.string()).default([])
|
|
18113
|
-
// Embedded IDs for ordering; optional in the file, [] = alphabetical
|
|
18114
|
-
}).refine((data) => data.entries && data.entries.length > 0, {
|
|
18115
|
-
message: "Collection must have at least one entry type"
|
|
18116
|
-
});
|
|
18117
|
-
rootCollectionMetaSchema2 = z16.object({
|
|
18118
|
-
label: z16.string().optional(),
|
|
18119
|
-
entries: z16.array(entryTypeMetaSchema2).optional(),
|
|
18120
|
-
order: z16.array(z16.string()).optional()
|
|
18121
|
-
// Embedded IDs for ordering items
|
|
18122
|
-
});
|
|
18398
|
+
init_validation3();
|
|
18399
|
+
init_branch();
|
|
18123
18400
|
}
|
|
18124
18401
|
});
|
|
18125
18402
|
|
|
18126
|
-
// dist/
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18403
|
+
// dist/utils/async-mutex.js
|
|
18404
|
+
async function withLock2(key, fn) {
|
|
18405
|
+
const prev = locks2.get(key) ?? Promise.resolve();
|
|
18406
|
+
let resolve2;
|
|
18407
|
+
const next = new Promise((r) => {
|
|
18408
|
+
resolve2 = r;
|
|
18409
|
+
});
|
|
18410
|
+
locks2.set(key, next);
|
|
18411
|
+
await prev;
|
|
18412
|
+
try {
|
|
18413
|
+
return await fn();
|
|
18414
|
+
} finally {
|
|
18415
|
+
resolve2();
|
|
18416
|
+
if (locks2.get(key) === next)
|
|
18417
|
+
locks2.delete(key);
|
|
18134
18418
|
}
|
|
18135
|
-
});
|
|
18136
|
-
|
|
18137
|
-
// dist/content-index-generation.js
|
|
18138
|
-
async function bumpContentIndexGeneration2(root) {
|
|
18139
|
-
return bumpResourceGeneration2(root, RESOURCE3);
|
|
18140
|
-
}
|
|
18141
|
-
async function invalidateBranchContentCaches2(root) {
|
|
18142
|
-
await bumpContentIndexGeneration2(root);
|
|
18143
|
-
await bumpResourceGeneration2(root, SCHEMA_GENERATION_RESOURCE2);
|
|
18144
|
-
invalidateContentIndexesForRoot2(root);
|
|
18145
18419
|
}
|
|
18146
|
-
var
|
|
18147
|
-
var
|
|
18148
|
-
"dist/
|
|
18420
|
+
var locks2;
|
|
18421
|
+
var init_async_mutex = __esm({
|
|
18422
|
+
"dist/utils/async-mutex.js"() {
|
|
18149
18423
|
"use strict";
|
|
18150
|
-
|
|
18151
|
-
init_content_index_registry();
|
|
18152
|
-
init_branch_schema_cache();
|
|
18153
|
-
RESOURCE3 = "content-index";
|
|
18424
|
+
locks2 = /* @__PURE__ */ new Map();
|
|
18154
18425
|
}
|
|
18155
18426
|
});
|
|
18156
18427
|
|
|
18157
18428
|
// dist/utils/provisioning-lock.js
|
|
18158
|
-
import
|
|
18159
|
-
import
|
|
18429
|
+
import fs31 from "node:fs/promises";
|
|
18430
|
+
import path39 from "node:path";
|
|
18160
18431
|
import lockfile3 from "proper-lockfile";
|
|
18161
18432
|
function guardOnCompromised2(handler, lockPath) {
|
|
18162
18433
|
return (err2) => {
|
|
@@ -18202,14 +18473,14 @@ function releaseIgnoringAlreadyReleased2(release, lockPath) {
|
|
|
18202
18473
|
}
|
|
18203
18474
|
async function tryAcquireProvisioningLock2(lockTargetDir3, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS2, createParents = true) {
|
|
18204
18475
|
if (createParents) {
|
|
18205
|
-
await
|
|
18476
|
+
await fs31.mkdir(lockTargetDir3, { recursive: true });
|
|
18206
18477
|
} else {
|
|
18207
|
-
await
|
|
18478
|
+
await fs31.mkdir(lockTargetDir3).catch((err2) => {
|
|
18208
18479
|
if (!isNodeError(err2) || err2.code !== "EEXIST")
|
|
18209
18480
|
throw err2;
|
|
18210
18481
|
});
|
|
18211
18482
|
}
|
|
18212
|
-
const lockPath =
|
|
18483
|
+
const lockPath = path39.join(lockTargetDir3, lockName);
|
|
18213
18484
|
const release = await lockfile3.lock(lockPath, provisioningLockOptions2(lockPath, 0, onCompromised, staleMs));
|
|
18214
18485
|
return releaseIgnoringAlreadyReleased2(release, lockPath);
|
|
18215
18486
|
}
|
|
@@ -18223,1473 +18494,1556 @@ var init_provisioning_lock = __esm({
|
|
|
18223
18494
|
}
|
|
18224
18495
|
});
|
|
18225
18496
|
|
|
18226
|
-
// dist/utils/
|
|
18227
|
-
import
|
|
18228
|
-
import
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18497
|
+
// dist/utils/occ-json-write.js
|
|
18498
|
+
import fs33 from "node:fs/promises";
|
|
18499
|
+
import path40 from "node:path";
|
|
18500
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
18501
|
+
import lockfile4 from "proper-lockfile";
|
|
18502
|
+
async function writeOccJsonFile2(filePath, payload, options) {
|
|
18503
|
+
const { expectedVersion, settleMs = 50, trailingNewline = false } = options;
|
|
18504
|
+
const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
|
|
18505
|
+
const writeId = randomUUID6();
|
|
18506
|
+
const data = { ...payload, version: newVersion, writeId };
|
|
18507
|
+
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
18508
|
+
const dir = path40.dirname(filePath);
|
|
18509
|
+
await fs33.mkdir(dir, { recursive: true });
|
|
18510
|
+
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
18511
|
+
await fs33.writeFile(tempPath, content, "utf-8");
|
|
18512
|
+
if (expectedVersion === null) {
|
|
18513
|
+
try {
|
|
18514
|
+
await fs33.link(tempPath, filePath);
|
|
18515
|
+
} catch (err2) {
|
|
18516
|
+
await fs33.unlink(tempPath).catch(() => {
|
|
18517
|
+
});
|
|
18518
|
+
if (isFileExistsError(err2)) {
|
|
18519
|
+
throw new OccWriteConflictError2();
|
|
18520
|
+
}
|
|
18521
|
+
throw err2;
|
|
18522
|
+
}
|
|
18523
|
+
await fs33.unlink(tempPath).catch(() => {
|
|
18524
|
+
});
|
|
18525
|
+
return { version: newVersion, writeId };
|
|
18526
|
+
}
|
|
18527
|
+
try {
|
|
18528
|
+
let currentVersion = null;
|
|
18529
|
+
try {
|
|
18530
|
+
const current = JSON.parse(await fs33.readFile(filePath, "utf-8"));
|
|
18531
|
+
currentVersion = current.version ?? 0;
|
|
18532
|
+
} catch {
|
|
18533
|
+
currentVersion = null;
|
|
18534
|
+
}
|
|
18535
|
+
if (currentVersion !== expectedVersion) {
|
|
18536
|
+
throw new OccWriteConflictError2();
|
|
18537
|
+
}
|
|
18538
|
+
await fs33.rename(tempPath, filePath);
|
|
18539
|
+
if (settleMs > 0) {
|
|
18540
|
+
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
18541
|
+
}
|
|
18542
|
+
const afterWrite = JSON.parse(await fs33.readFile(filePath, "utf-8"));
|
|
18543
|
+
if (afterWrite.writeId !== writeId) {
|
|
18544
|
+
throw new OccWriteConflictError2();
|
|
18545
|
+
}
|
|
18546
|
+
} catch (err2) {
|
|
18547
|
+
await fs33.unlink(tempPath).catch(() => {
|
|
18548
|
+
});
|
|
18549
|
+
throw err2;
|
|
18550
|
+
}
|
|
18551
|
+
return { version: newVersion, writeId };
|
|
18234
18552
|
}
|
|
18235
|
-
async function
|
|
18236
|
-
const
|
|
18237
|
-
|
|
18553
|
+
async function withOccRetry2(operation, options) {
|
|
18554
|
+
const maxAttempts = options?.maxAttempts ?? 10;
|
|
18555
|
+
const isRetryable = options?.isRetryable;
|
|
18556
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
18238
18557
|
try {
|
|
18239
|
-
return await
|
|
18558
|
+
return await operation();
|
|
18240
18559
|
} catch (err2) {
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18560
|
+
const retryable = err2 instanceof OccWriteConflictError2 || (isRetryable?.(err2) ?? false);
|
|
18561
|
+
if (retryable && attempt < maxAttempts) {
|
|
18562
|
+
const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
|
|
18563
|
+
const jitter = Math.random() * baseDelay;
|
|
18564
|
+
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
18565
|
+
continue;
|
|
18566
|
+
}
|
|
18567
|
+
throw err2;
|
|
18248
18568
|
}
|
|
18249
18569
|
}
|
|
18570
|
+
throw new Error("Unreachable");
|
|
18250
18571
|
}
|
|
18251
|
-
async function
|
|
18252
|
-
let compromised = false;
|
|
18253
|
-
const release = await acquireContentWriteLock2(branchRoot, waitMs, (err2) => {
|
|
18254
|
-
compromised = true;
|
|
18255
|
-
canopyLogWarn3(`[canopy] Content-write lock compromised mid-hold for ${branchRoot}:`, getErrorMessage(err2));
|
|
18256
|
-
});
|
|
18257
|
-
let result;
|
|
18572
|
+
async function withOccFileLock2(filePath, fn) {
|
|
18258
18573
|
try {
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18574
|
+
await fs33.mkdir(path40.dirname(filePath));
|
|
18575
|
+
} catch (err2) {
|
|
18576
|
+
if (!isNodeError(err2) || err2.code !== "EEXIST") {
|
|
18577
|
+
throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err2)}`);
|
|
18578
|
+
}
|
|
18264
18579
|
}
|
|
18265
|
-
|
|
18266
|
-
|
|
18580
|
+
const MAX_ATTEMPTS = 14;
|
|
18581
|
+
let release = null;
|
|
18582
|
+
for (let attempt = 1; release === null; attempt++) {
|
|
18583
|
+
try {
|
|
18584
|
+
release = await lockfile4.lock(filePath, {
|
|
18585
|
+
lockfilePath: `${filePath}.lock`,
|
|
18586
|
+
realpath: false,
|
|
18587
|
+
stale: 1e4,
|
|
18588
|
+
retries: 0,
|
|
18589
|
+
// A compromised lock (the lock dir vanished or refresh failed mid-hold, e.g. the branch
|
|
18590
|
+
// directory containing it was deleted, or an EFS waiter misread a cached mtime and took
|
|
18591
|
+
// it over) must not crash the process, which is proper-lockfile's default. Our critical
|
|
18592
|
+
// sections are short and idempotent-on-conflict (OCC verify inside); log and let the
|
|
18593
|
+
// section finish.
|
|
18594
|
+
onCompromised: guardOnCompromised2((err2) => canopyLogWarn3(`[canopy] OCC file lock compromised mid-hold for ${filePath}:`, getErrorMessage(err2)), filePath)
|
|
18595
|
+
});
|
|
18596
|
+
} catch (err2) {
|
|
18597
|
+
const code = isNodeError(err2) ? err2.code : void 0;
|
|
18598
|
+
const contended = code === "ELOCKED";
|
|
18599
|
+
if (!contended || attempt >= MAX_ATTEMPTS) {
|
|
18600
|
+
throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err2)}`);
|
|
18601
|
+
}
|
|
18602
|
+
const baseDelay = Math.min(50 * Math.pow(1.6, attempt - 1), 2e3);
|
|
18603
|
+
const jitter = Math.random() * baseDelay;
|
|
18604
|
+
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
18605
|
+
}
|
|
18606
|
+
}
|
|
18607
|
+
try {
|
|
18608
|
+
return await fn();
|
|
18609
|
+
} finally {
|
|
18610
|
+
await release().catch((err2) => {
|
|
18611
|
+
if (isNodeError(err2) && err2.code === "ENOENT")
|
|
18612
|
+
return;
|
|
18613
|
+
log13.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err2) });
|
|
18614
|
+
});
|
|
18267
18615
|
}
|
|
18268
|
-
return result;
|
|
18269
18616
|
}
|
|
18270
|
-
var
|
|
18271
|
-
var
|
|
18272
|
-
"dist/utils/
|
|
18617
|
+
var log13, OccWriteConflictError2;
|
|
18618
|
+
var init_occ_json_write = __esm({
|
|
18619
|
+
"dist/utils/occ-json-write.js"() {
|
|
18273
18620
|
"use strict";
|
|
18274
18621
|
init_error();
|
|
18622
|
+
init_debug2();
|
|
18275
18623
|
init_logger3();
|
|
18276
18624
|
init_provisioning_lock();
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
ContentWriteLockBusyError2 = class extends Error {
|
|
18281
|
-
constructor(message = "This branch is busy syncing or saving another change, so your change was not saved. Try again in a moment.", outcome = "not-run") {
|
|
18625
|
+
log13 = createDebugLogger3({ prefix: "OccJsonWrite" });
|
|
18626
|
+
OccWriteConflictError2 = class extends Error {
|
|
18627
|
+
constructor(message = "Concurrent modification detected") {
|
|
18282
18628
|
super(message);
|
|
18283
|
-
this.
|
|
18284
|
-
this.name = "ContentWriteLockBusyError";
|
|
18629
|
+
this.name = "OccWriteConflictError";
|
|
18285
18630
|
}
|
|
18286
18631
|
};
|
|
18287
|
-
sleep3 = (ms) => new Promise((resolve2) => {
|
|
18288
|
-
setTimeout(resolve2, ms);
|
|
18289
|
-
});
|
|
18290
|
-
CONTENT_WRITE_LOCK_STALE_MS2 = 3e4;
|
|
18291
|
-
heldRoots2 = new AsyncLocalStorage3();
|
|
18292
18632
|
}
|
|
18293
18633
|
});
|
|
18294
18634
|
|
|
18295
|
-
// dist/
|
|
18296
|
-
import
|
|
18297
|
-
import
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
const normalizedResolved = path38.resolve(resolved);
|
|
18301
|
-
const normalizedParent = path38.resolve(parent);
|
|
18302
|
-
if (!normalizedResolved.startsWith(normalizedParent + path38.sep) && normalizedResolved !== normalizedParent) {
|
|
18303
|
-
throw new Error(`${label} escapes the expected directory: ${resolved}`);
|
|
18304
|
-
}
|
|
18305
|
-
}
|
|
18306
|
-
async function safeReplaceDir(oldDir, newDir) {
|
|
18307
|
-
const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
18308
|
-
const oldExists = await filePathExists(oldDir);
|
|
18309
|
-
if (oldExists) {
|
|
18310
|
-
await fs31.rename(oldDir, backupDir);
|
|
18311
|
-
}
|
|
18312
|
-
try {
|
|
18313
|
-
await fs31.rename(newDir, oldDir);
|
|
18314
|
-
} catch (err2) {
|
|
18315
|
-
if (oldExists) {
|
|
18316
|
-
await fs31.rename(backupDir, oldDir).catch(() => {
|
|
18317
|
-
});
|
|
18318
|
-
}
|
|
18319
|
-
throw err2;
|
|
18320
|
-
}
|
|
18321
|
-
if (oldExists) {
|
|
18322
|
-
await fs31.rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
18323
|
-
});
|
|
18324
|
-
}
|
|
18325
|
-
}
|
|
18326
|
-
async function listFilesRecursive(dir, prefix = "") {
|
|
18327
|
-
const results = [];
|
|
18328
|
-
let entries;
|
|
18329
|
-
try {
|
|
18330
|
-
entries = await fs31.readdir(dir, { withFileTypes: true });
|
|
18331
|
-
} catch {
|
|
18332
|
-
return results;
|
|
18333
|
-
}
|
|
18334
|
-
for (const entry of entries) {
|
|
18335
|
-
if (entry.name === ".git")
|
|
18336
|
-
continue;
|
|
18337
|
-
if (entry.isSymbolicLink())
|
|
18338
|
-
continue;
|
|
18339
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
18340
|
-
if (entry.isDirectory()) {
|
|
18341
|
-
results.push(...await listFilesRecursive(path38.join(dir, entry.name), rel));
|
|
18342
|
-
} else {
|
|
18343
|
-
results.push(rel);
|
|
18344
|
-
}
|
|
18345
|
-
}
|
|
18346
|
-
return results;
|
|
18347
|
-
}
|
|
18348
|
-
async function copyDir(src, dest) {
|
|
18349
|
-
await fs31.mkdir(dest, { recursive: true });
|
|
18350
|
-
const entries = await fs31.readdir(src, { withFileTypes: true });
|
|
18351
|
-
for (const entry of entries) {
|
|
18352
|
-
if (entry.name === ".git")
|
|
18353
|
-
continue;
|
|
18354
|
-
if (entry.isSymbolicLink())
|
|
18355
|
-
continue;
|
|
18356
|
-
const srcPath = path38.join(src, entry.name);
|
|
18357
|
-
const destPath = path38.join(dest, entry.name);
|
|
18358
|
-
if (entry.isDirectory()) {
|
|
18359
|
-
await copyDir(srcPath, destPath);
|
|
18360
|
-
} else {
|
|
18361
|
-
await fs31.copyFile(srcPath, destPath);
|
|
18362
|
-
}
|
|
18363
|
-
}
|
|
18635
|
+
// dist/branch-metadata.js
|
|
18636
|
+
import fs34 from "node:fs/promises";
|
|
18637
|
+
import path41 from "node:path";
|
|
18638
|
+
function isRecordedEditor2(branch, userId) {
|
|
18639
|
+
return (branch.editors?.includes(userId) ?? false) && (branch.uncommittedEditors?.includes(userId) ?? false);
|
|
18364
18640
|
}
|
|
18365
|
-
|
|
18366
|
-
|
|
18367
|
-
if (!await filePathExists(srcContentDir)) {
|
|
18368
|
-
return { fileCount: 0, savedEditorState: false };
|
|
18369
|
-
}
|
|
18370
|
-
const wsContentDir = path38.join(branchPath, contentRoot);
|
|
18371
|
-
assertWithinDir(wsContentDir, branchPath, "content-root");
|
|
18372
|
-
return withContentWriteLock2(branchPath, () => replaceWorkspaceContent(options, wsContentDir));
|
|
18641
|
+
function withMember2(list, id) {
|
|
18642
|
+
return list?.includes(id) ? [...list] : [...list ?? [], id];
|
|
18373
18643
|
}
|
|
18374
|
-
|
|
18375
|
-
const
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
|
|
18396
|
-
|
|
18397
|
-
|
|
18398
|
-
|
|
18399
|
-
|
|
18400
|
-
|
|
18401
|
-
|
|
18644
|
+
function mergeBranchMetadata2(existing, version, incoming, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
18645
|
+
const defaults = {
|
|
18646
|
+
name: "unknown",
|
|
18647
|
+
status: "editing",
|
|
18648
|
+
access: {},
|
|
18649
|
+
createdBy: "unknown",
|
|
18650
|
+
createdAt: now,
|
|
18651
|
+
updatedAt: now
|
|
18652
|
+
};
|
|
18653
|
+
return {
|
|
18654
|
+
// Envelope keys this code does not know survive, as unknown branch keys do below.
|
|
18655
|
+
...existing,
|
|
18656
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
18657
|
+
version: version ?? 0,
|
|
18658
|
+
branch: {
|
|
18659
|
+
...defaults,
|
|
18660
|
+
...existing?.branch,
|
|
18661
|
+
...incoming.branch,
|
|
18662
|
+
access: {
|
|
18663
|
+
...existing?.branch?.access,
|
|
18664
|
+
...incoming.branch?.access
|
|
18665
|
+
},
|
|
18666
|
+
// Immutable after creation
|
|
18667
|
+
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
18668
|
+
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
18669
|
+
// Fork point is recorded once at creation; later saves must not move it
|
|
18670
|
+
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
18671
|
+
// Always stamped fresh; the spreads above would otherwise let the creation-time value win
|
|
18672
|
+
// forever, freezing the timestamp the editor's Branches panel sorts and displays by
|
|
18673
|
+
updatedAt: now
|
|
18402
18674
|
}
|
|
18403
|
-
|
|
18404
|
-
if (baseTag)
|
|
18405
|
-
await wsGit.tag(["-f", baseTag]);
|
|
18406
|
-
return { fileCount: staged.length, savedEditorState };
|
|
18407
|
-
} finally {
|
|
18408
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
18409
|
-
}
|
|
18410
|
-
}
|
|
18411
|
-
var SYNC_BASE_TAG;
|
|
18412
|
-
var init_sync_core = __esm({
|
|
18413
|
-
"dist/sync-core.js"() {
|
|
18414
|
-
"use strict";
|
|
18415
|
-
init_content_index_generation();
|
|
18416
|
-
init_content_write_lock();
|
|
18417
|
-
init_fs();
|
|
18418
|
-
init_git();
|
|
18419
|
-
SYNC_BASE_TAG = "canopycms-sync-base";
|
|
18420
|
-
}
|
|
18421
|
-
});
|
|
18422
|
-
|
|
18423
|
-
// dist/git-manager.js
|
|
18424
|
-
import fs33 from "node:fs/promises";
|
|
18425
|
-
import path39 from "node:path";
|
|
18426
|
-
import { simpleGit as simpleGit6 } from "simple-git";
|
|
18427
|
-
async function ensureGitExcludePattern2(repoPath, pattern) {
|
|
18428
|
-
const excludePath = path39.join(repoPath, ".git", "info", "exclude");
|
|
18429
|
-
await fs33.mkdir(path39.dirname(excludePath), { recursive: true });
|
|
18430
|
-
let content = "";
|
|
18431
|
-
try {
|
|
18432
|
-
content = await fs33.readFile(excludePath, "utf-8");
|
|
18433
|
-
} catch (err2) {
|
|
18434
|
-
if (!isNotFoundError(err2))
|
|
18435
|
-
throw err2;
|
|
18436
|
-
}
|
|
18437
|
-
const lines = content.split("\n");
|
|
18438
|
-
if (lines.some((line) => line.trim() === pattern)) {
|
|
18439
|
-
log12.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
18440
|
-
return;
|
|
18441
|
-
}
|
|
18442
|
-
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
18443
|
-
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
18444
|
-
await fs33.writeFile(excludePath, newContent, "utf-8");
|
|
18445
|
-
log12.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
18675
|
+
};
|
|
18446
18676
|
}
|
|
18447
|
-
var
|
|
18448
|
-
var
|
|
18449
|
-
"dist/
|
|
18677
|
+
var CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataFileManager2, loadBranchContext2;
|
|
18678
|
+
var init_branch_metadata = __esm({
|
|
18679
|
+
"dist/branch-metadata.js"() {
|
|
18450
18680
|
"use strict";
|
|
18451
|
-
|
|
18681
|
+
init_branch_registry();
|
|
18682
|
+
init_branch_metadata_file();
|
|
18683
|
+
init_paths();
|
|
18452
18684
|
init_error();
|
|
18453
|
-
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
|
|
18457
|
-
|
|
18458
|
-
|
|
18459
|
-
|
|
18460
|
-
sync: () => sync
|
|
18461
|
-
});
|
|
18462
|
-
import fs34 from "node:fs/promises";
|
|
18463
|
-
import path40 from "node:path";
|
|
18464
|
-
import { simpleGit as simpleGit7 } from "simple-git";
|
|
18465
|
-
import * as p2 from "@clack/prompts";
|
|
18466
|
-
function lockBusyAsSyncError(err2, branchName) {
|
|
18467
|
-
if (!(err2 instanceof ContentWriteLockBusyError2))
|
|
18468
|
-
return err2;
|
|
18469
|
-
return new SyncError(err2.outcome === "not-run" ? `Branch workspace "${branchName}" is busy (the worker is syncing it, or an editor save is in flight); nothing was changed. Try again in a moment.` : `Branch workspace "${branchName}" was being synced during this run, so it may be partly updated. Check its state before running sync again.`);
|
|
18470
|
-
}
|
|
18471
|
-
async function withWorkspaceLock(branchPath, branchName, fn) {
|
|
18472
|
-
try {
|
|
18473
|
-
return await withContentWriteLock2(branchPath, fn);
|
|
18474
|
-
} catch (err2) {
|
|
18475
|
-
throw lockBusyAsSyncError(err2, branchName);
|
|
18476
|
-
}
|
|
18477
|
-
}
|
|
18478
|
-
async function abortWorkspaceMerge(branchPath, branchName) {
|
|
18479
|
-
return withWorkspaceLock(branchPath, branchName, async () => {
|
|
18480
|
-
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
18481
|
-
if ((await wsGit.status()).conflicted.length === 0)
|
|
18482
|
-
return false;
|
|
18483
|
-
await wsGit.merge(["--abort"]);
|
|
18484
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
18485
|
-
return true;
|
|
18486
|
-
});
|
|
18487
|
-
}
|
|
18488
|
-
async function detectCurrentBranch(projectDir) {
|
|
18489
|
-
const git = simpleGit7({ baseDir: projectDir });
|
|
18490
|
-
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
18491
|
-
if (branch === "HEAD")
|
|
18492
|
-
throw new Error("Detached HEAD \u2014 use --branch to specify a branch name");
|
|
18493
|
-
return branch;
|
|
18494
|
-
}
|
|
18495
|
-
async function selectBranch(options, branchesDir) {
|
|
18496
|
-
let branches = [];
|
|
18497
|
-
if (await filePathExists(branchesDir)) {
|
|
18498
|
-
const entries = await fs34.readdir(branchesDir, { withFileTypes: true });
|
|
18499
|
-
branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
18500
|
-
}
|
|
18501
|
-
let branchName = options.branch;
|
|
18502
|
-
if (!branchName) {
|
|
18503
|
-
try {
|
|
18504
|
-
branchName = await detectCurrentBranch(options.projectDir);
|
|
18505
|
-
} catch {
|
|
18506
|
-
}
|
|
18507
|
-
}
|
|
18508
|
-
if (!branchName) {
|
|
18509
|
-
if (branches.length === 0) {
|
|
18510
|
-
throw new SyncError("No branch workspaces found and could not detect current git branch. Use --branch to specify.");
|
|
18511
|
-
}
|
|
18512
|
-
if (branches.length === 1) {
|
|
18513
|
-
branchName = branches[0];
|
|
18514
|
-
} else {
|
|
18515
|
-
const result = await p2.select({
|
|
18516
|
-
message: "Which branch?",
|
|
18517
|
-
options: branches.map((b) => ({ value: b, label: b }))
|
|
18518
|
-
});
|
|
18519
|
-
if (p2.isCancel(result)) {
|
|
18520
|
-
p2.cancel("Sync cancelled.");
|
|
18521
|
-
return null;
|
|
18685
|
+
init_async_mutex();
|
|
18686
|
+
init_occ_json_write();
|
|
18687
|
+
CURRENT_SCHEMA_VERSION2 = 1;
|
|
18688
|
+
BranchMetadataConflictError2 = class extends Error {
|
|
18689
|
+
constructor(message = "Concurrent modification detected in branch metadata") {
|
|
18690
|
+
super(message);
|
|
18691
|
+
this.name = "BranchMetadataConflictError";
|
|
18522
18692
|
}
|
|
18523
|
-
|
|
18524
|
-
|
|
18525
|
-
|
|
18526
|
-
|
|
18527
|
-
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
|
|
18531
|
-
|
|
18532
|
-
|
|
18533
|
-
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
|
|
18560
|
-
|
|
18561
|
-
|
|
18562
|
-
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
|
|
18568
|
-
|
|
18569
|
-
|
|
18693
|
+
};
|
|
18694
|
+
BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
|
|
18695
|
+
constructor(branchRoot, baseRoot, options) {
|
|
18696
|
+
this.branchRoot = path41.resolve(branchRoot);
|
|
18697
|
+
this.filePath = path41.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
18698
|
+
this.baseRoot = baseRoot;
|
|
18699
|
+
this.settleMs = options?.settleMs;
|
|
18700
|
+
}
|
|
18701
|
+
/** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
|
|
18702
|
+
static async loadOnly(branchRoot) {
|
|
18703
|
+
return readBranchMetadataFile2(branchRoot);
|
|
18704
|
+
}
|
|
18705
|
+
/**
|
|
18706
|
+
* An instance wired for registry invalidation. API handlers use this, so the
|
|
18707
|
+
* registry cache is invalidated on update.
|
|
18708
|
+
*/
|
|
18709
|
+
static get(branchRoot, baseRoot, options) {
|
|
18710
|
+
return new _BranchMetadataFileManager2(branchRoot, baseRoot, options);
|
|
18711
|
+
}
|
|
18712
|
+
/**
|
|
18713
|
+
* The file the save cycle merges over. A corrupt one throws
|
|
18714
|
+
* {@link BranchMetadataCorruptError} rather than reading as absent, so a save
|
|
18715
|
+
* never lays defaults (status 'editing', no ACL) over a branch whose real
|
|
18716
|
+
* state is unknown; repair-metadata archives the file before it saves.
|
|
18717
|
+
*/
|
|
18718
|
+
async load() {
|
|
18719
|
+
const meta = await readBranchMetadataFile2(this.branchRoot);
|
|
18720
|
+
return { meta, version: meta?.version ?? null };
|
|
18721
|
+
}
|
|
18722
|
+
/**
|
|
18723
|
+
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
18724
|
+
* default here since payload shaping is branch-metadata's concern.
|
|
18725
|
+
*
|
|
18726
|
+
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
18727
|
+
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
18728
|
+
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
18729
|
+
* save() boundary would make withOccRetry's predicate miss it.
|
|
18730
|
+
*
|
|
18731
|
+
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
18732
|
+
* unlike comments.json.
|
|
18733
|
+
*/
|
|
18734
|
+
async write(meta, expectedVersion) {
|
|
18735
|
+
const payload = {
|
|
18736
|
+
...meta,
|
|
18737
|
+
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION2
|
|
18738
|
+
};
|
|
18739
|
+
return writeOccJsonFile2(this.filePath, payload, {
|
|
18740
|
+
expectedVersion,
|
|
18741
|
+
settleMs: this.settleMs,
|
|
18742
|
+
trailingNewline: true
|
|
18743
|
+
});
|
|
18744
|
+
}
|
|
18745
|
+
/**
|
|
18746
|
+
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
18747
|
+
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
18748
|
+
*
|
|
18749
|
+
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
18750
|
+
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
18751
|
+
* removes the branch directory, while the save reaches here after it, and
|
|
18752
|
+
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
18753
|
+
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
18754
|
+
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
18755
|
+
* save fails fast instead of paying for a lock.
|
|
18756
|
+
*
|
|
18757
|
+
* A save that passes the check just before a delete still cannot write: the
|
|
18758
|
+
* delete renames the tree away under the same lockfile, and that lockfile
|
|
18759
|
+
* creates only its own directory (`withOccFileLock`), so the save fails there.
|
|
18760
|
+
*/
|
|
18761
|
+
async save(incoming) {
|
|
18762
|
+
const saved = await this.saveIf(incoming, () => true);
|
|
18763
|
+
if (saved === null)
|
|
18764
|
+
throw new Error("unreachable: an unconditional save was skipped");
|
|
18765
|
+
return saved;
|
|
18766
|
+
}
|
|
18767
|
+
/**
|
|
18768
|
+
* {@link save}, applied only when `onlyIf` accepts the metadata on disk; resolves null when it
|
|
18769
|
+
* does not, having written nothing. `onlyIf` runs inside the lock stack against the version
|
|
18770
|
+
* the write is checked against, so the decision and the write are one compare-and-set.
|
|
18771
|
+
*/
|
|
18772
|
+
async saveIf(incoming, onlyIf) {
|
|
18773
|
+
return this.update((existing) => onlyIf(existing) ? incoming : null);
|
|
18774
|
+
}
|
|
18775
|
+
/**
|
|
18776
|
+
* A save whose update `compute` derives from the metadata on disk, inside the lock stack, so
|
|
18777
|
+
* an update built from the existing value (adding to a list) cannot lose a concurrent one.
|
|
18778
|
+
* `compute` returning null writes nothing and resolves null. `keepUpdatedAt` leaves
|
|
18779
|
+
* `updatedAt` as it was, for bookkeeping writes that are not a change to the branch.
|
|
18780
|
+
*/
|
|
18781
|
+
async update(compute, options = {}) {
|
|
18782
|
+
try {
|
|
18783
|
+
await fs34.stat(this.branchRoot);
|
|
18784
|
+
} catch (err2) {
|
|
18785
|
+
if (isNotFoundError(err2)) {
|
|
18786
|
+
throw new BranchMetadataConflictError2("Branch no longer exists");
|
|
18787
|
+
}
|
|
18788
|
+
throw err2;
|
|
18789
|
+
}
|
|
18790
|
+
let saved;
|
|
18791
|
+
try {
|
|
18792
|
+
saved = await withLock2(this.filePath, () => withOccFileLock2(this.filePath, () => withOccRetry2(async () => {
|
|
18793
|
+
const { meta: existing, version } = await this.load();
|
|
18794
|
+
const incoming = compute(existing);
|
|
18795
|
+
if (incoming === null)
|
|
18796
|
+
return null;
|
|
18797
|
+
const merged = mergeBranchMetadata2(existing, version, incoming);
|
|
18798
|
+
if (options.keepUpdatedAt && existing)
|
|
18799
|
+
merged.branch.updatedAt = existing.branch.updatedAt;
|
|
18800
|
+
const written = await this.write(merged, version);
|
|
18801
|
+
merged.version = written.version;
|
|
18802
|
+
merged.writeId = written.writeId;
|
|
18803
|
+
return merged;
|
|
18804
|
+
})));
|
|
18805
|
+
} catch (err2) {
|
|
18806
|
+
if (err2 instanceof OccWriteConflictError2) {
|
|
18807
|
+
throw new BranchMetadataConflictError2();
|
|
18808
|
+
}
|
|
18809
|
+
throw err2;
|
|
18570
18810
|
}
|
|
18811
|
+
if (saved !== null)
|
|
18812
|
+
await this.invalidateRegistry();
|
|
18813
|
+
return saved;
|
|
18571
18814
|
}
|
|
18572
|
-
|
|
18573
|
-
|
|
18815
|
+
/**
|
|
18816
|
+
* Adds `userId` to `editors` and `uncommittedEditors`. A lock-free read skips the write when
|
|
18817
|
+
* both already hold it, so only a user's first save since the last submit commit pays for the lock.
|
|
18818
|
+
* Never creates branch.json.
|
|
18819
|
+
*/
|
|
18820
|
+
async recordEditor(userId) {
|
|
18821
|
+
const current = await readBranchMetadataFile2(this.branchRoot);
|
|
18822
|
+
if (!current || isRecordedEditor2(current.branch, userId))
|
|
18823
|
+
return;
|
|
18824
|
+
await this.update((existing) => existing && !isRecordedEditor2(existing.branch, userId) ? {
|
|
18825
|
+
branch: {
|
|
18826
|
+
editors: withMember2(existing.branch.editors, userId),
|
|
18827
|
+
uncommittedEditors: withMember2(existing.branch.uncommittedEditors, userId)
|
|
18828
|
+
}
|
|
18829
|
+
} : null, { keepUpdatedAt: true });
|
|
18830
|
+
}
|
|
18831
|
+
/**
|
|
18832
|
+
* Removes `userIds` from `uncommittedEditors` once a submit commit's trailers name them. Ids
|
|
18833
|
+
* recorded after the commit read them stay, so the next commit names them.
|
|
18834
|
+
*/
|
|
18835
|
+
async markEditorsCommitted(userIds) {
|
|
18836
|
+
const committed = new Set(userIds);
|
|
18837
|
+
if (committed.size === 0)
|
|
18838
|
+
return;
|
|
18839
|
+
await this.update((existing) => {
|
|
18840
|
+
const pending = existing?.branch.uncommittedEditors ?? [];
|
|
18841
|
+
const remaining = pending.filter((id) => !committed.has(id));
|
|
18842
|
+
if (remaining.length === pending.length)
|
|
18843
|
+
return null;
|
|
18844
|
+
return { branch: { uncommittedEditors: remaining.length > 0 ? remaining : void 0 } };
|
|
18845
|
+
}, { keepUpdatedAt: true });
|
|
18846
|
+
}
|
|
18847
|
+
/** Invalidate the registry cache so the next list() regenerates. */
|
|
18848
|
+
async invalidateRegistry() {
|
|
18849
|
+
const registry3 = new BranchRegistry2(this.baseRoot);
|
|
18850
|
+
await registry3.invalidate();
|
|
18851
|
+
}
|
|
18852
|
+
};
|
|
18853
|
+
loadBranchContext2 = async (options) => {
|
|
18854
|
+
const { branchRoot, baseRoot } = resolveBranchPath2({
|
|
18855
|
+
branchName: options.branchName,
|
|
18856
|
+
mode: options.mode,
|
|
18857
|
+
basePathOverride: options.basePathOverride
|
|
18858
|
+
});
|
|
18859
|
+
const meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
|
|
18860
|
+
if (!meta) {
|
|
18861
|
+
return null;
|
|
18862
|
+
}
|
|
18863
|
+
return {
|
|
18864
|
+
branch: meta.branch,
|
|
18865
|
+
branchRoot,
|
|
18866
|
+
baseRoot
|
|
18867
|
+
};
|
|
18868
|
+
};
|
|
18574
18869
|
}
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
}
|
|
18584
|
-
const confirm5 = await p2.confirm({
|
|
18585
|
-
message: "Continue? Editor changes will be preserved in git history.",
|
|
18586
|
-
initialValue: false
|
|
18587
|
-
});
|
|
18588
|
-
if (p2.isCancel(confirm5) || !confirm5) {
|
|
18589
|
-
p2.cancel("Push cancelled.");
|
|
18590
|
-
return { fileCount: 0 };
|
|
18591
|
-
}
|
|
18870
|
+
});
|
|
18871
|
+
|
|
18872
|
+
// dist/utils/base-branch.js
|
|
18873
|
+
function baseBranchOf2(config) {
|
|
18874
|
+
if (config.defaultBaseBranch)
|
|
18875
|
+
return config.defaultBaseBranch;
|
|
18876
|
+
if (config.mode === "prod") {
|
|
18877
|
+
throw new BaseBranchUnresolvedError2("the prod base branch has not been resolved.");
|
|
18592
18878
|
}
|
|
18593
|
-
|
|
18594
|
-
|
|
18879
|
+
return "main";
|
|
18880
|
+
}
|
|
18881
|
+
var BaseBranchUnresolvedError2;
|
|
18882
|
+
var init_base_branch = __esm({
|
|
18883
|
+
"dist/utils/base-branch.js"() {
|
|
18884
|
+
"use strict";
|
|
18885
|
+
BaseBranchUnresolvedError2 = class extends Error {
|
|
18886
|
+
constructor(detail) {
|
|
18887
|
+
super(`CanopyCMS: ${detail} Set defaultBaseBranch in your CanopyCMS config to the branch editing branches fork from.`);
|
|
18888
|
+
this.name = "BaseBranchUnresolvedError";
|
|
18889
|
+
}
|
|
18890
|
+
};
|
|
18891
|
+
}
|
|
18892
|
+
});
|
|
18893
|
+
|
|
18894
|
+
// dist/branch-param.js
|
|
18895
|
+
function cmsBranchNamespace2(config) {
|
|
18595
18896
|
try {
|
|
18596
|
-
|
|
18597
|
-
|
|
18598
|
-
|
|
18599
|
-
contentRoot,
|
|
18600
|
-
baseTag: SYNC_BASE_TAG,
|
|
18601
|
-
saveEditorStateMessage: "sync: save editor state before push"
|
|
18602
|
-
});
|
|
18603
|
-
fileCount = result.fileCount;
|
|
18604
|
-
if (result.savedEditorState)
|
|
18605
|
-
p2.log.info("Committed editor changes to history before push");
|
|
18606
|
-
} catch (err2) {
|
|
18607
|
-
throw lockBusyAsSyncError(err2, branchName);
|
|
18897
|
+
return operatingStrategy3(config.mode).getBranchNamespace(config);
|
|
18898
|
+
} catch {
|
|
18899
|
+
return null;
|
|
18608
18900
|
}
|
|
18609
|
-
|
|
18610
|
-
|
|
18611
|
-
|
|
18612
|
-
|
|
18901
|
+
}
|
|
18902
|
+
function namesBaseOrActive2(param, config) {
|
|
18903
|
+
let base;
|
|
18904
|
+
try {
|
|
18905
|
+
base = baseBranchOf2(config);
|
|
18906
|
+
} catch (err2) {
|
|
18907
|
+
if (err2 instanceof BaseBranchUnresolvedError2)
|
|
18908
|
+
return false;
|
|
18909
|
+
throw err2;
|
|
18613
18910
|
}
|
|
18614
|
-
return
|
|
18911
|
+
return param === base || param === (config.defaultActiveBranch ?? base);
|
|
18615
18912
|
}
|
|
18616
|
-
async function
|
|
18617
|
-
const {
|
|
18618
|
-
|
|
18619
|
-
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18624
|
-
|
|
18625
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18913
|
+
async function resolveBranchParam(param, config, options = {}) {
|
|
18914
|
+
const load = (branchName) => loadBranchContext2({ branchName, mode: config.mode, basePathOverride: options.basePathOverride });
|
|
18915
|
+
if (isExactBranchForm(param)) {
|
|
18916
|
+
const exact = exactBranchName(param);
|
|
18917
|
+
if (exact === null)
|
|
18918
|
+
return null;
|
|
18919
|
+
const named = await load(exact);
|
|
18920
|
+
return named?.branch.name === exact ? named : null;
|
|
18921
|
+
}
|
|
18922
|
+
if (namesBaseOrActive2(param, config))
|
|
18923
|
+
return load(param);
|
|
18924
|
+
const namespace = cmsBranchNamespace2(config);
|
|
18925
|
+
if (namespace !== null && !param.includes("/")) {
|
|
18926
|
+
const ref = `${namespace}${param}`;
|
|
18927
|
+
const inNamespace = await load(ref).catch((err2) => {
|
|
18928
|
+
if (namesNoWorkspace(err2))
|
|
18929
|
+
return null;
|
|
18930
|
+
throw err2;
|
|
18931
|
+
});
|
|
18932
|
+
if (inNamespace?.branch.name === ref)
|
|
18933
|
+
return inNamespace;
|
|
18629
18934
|
}
|
|
18630
|
-
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18634
|
-
|
|
18635
|
-
|
|
18636
|
-
|
|
18637
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
if (!branchFiles.has(file)) {
|
|
18642
|
-
const relativePath = contentRoot + "/" + file;
|
|
18643
|
-
if (!uncommittedContent.some((f) => f.path === relativePath)) {
|
|
18644
|
-
untrackedLosses.push(relativePath);
|
|
18645
|
-
}
|
|
18646
|
-
}
|
|
18647
|
-
}
|
|
18935
|
+
const outside = await load(param);
|
|
18936
|
+
return outside?.branch.name === param ? outside : null;
|
|
18937
|
+
}
|
|
18938
|
+
var init_branch_param = __esm({
|
|
18939
|
+
"dist/branch-param.js"() {
|
|
18940
|
+
"use strict";
|
|
18941
|
+
init_branch_metadata();
|
|
18942
|
+
init_operating_mode2();
|
|
18943
|
+
init_branch();
|
|
18944
|
+
init_validation3();
|
|
18945
|
+
init_base_branch();
|
|
18648
18946
|
}
|
|
18649
|
-
|
|
18650
|
-
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
|
|
18657
|
-
|
|
18658
|
-
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
18666
|
-
|
|
18947
|
+
});
|
|
18948
|
+
|
|
18949
|
+
// dist/utils/git.js
|
|
18950
|
+
import { simpleGit as simpleGit4 } from "simple-git";
|
|
18951
|
+
function isCanopyInternalPath(repoRelativePath) {
|
|
18952
|
+
return repoRelativePath === CANOPY_META_DIR2 || repoRelativePath.startsWith(`${CANOPY_META_DIR2}/`);
|
|
18953
|
+
}
|
|
18954
|
+
async function stageAllExceptCanopyState2(git) {
|
|
18955
|
+
await git.raw(["add", "-A", "--sparse"]);
|
|
18956
|
+
await git.raw(["reset", "-q", "--", CANOPY_META_DIR2]);
|
|
18957
|
+
}
|
|
18958
|
+
async function detectHeadBranch2(repoRoot, fallback = "main") {
|
|
18959
|
+
try {
|
|
18960
|
+
const git = simpleGit4({ baseDir: repoRoot });
|
|
18961
|
+
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
18962
|
+
return head && head !== "HEAD" ? head : fallback;
|
|
18963
|
+
} catch {
|
|
18964
|
+
return fallback;
|
|
18965
|
+
}
|
|
18966
|
+
}
|
|
18967
|
+
var CANOPY_META_DIR2;
|
|
18968
|
+
var init_git = __esm({
|
|
18969
|
+
"dist/utils/git.js"() {
|
|
18970
|
+
"use strict";
|
|
18971
|
+
CANOPY_META_DIR2 = ".canopy-meta";
|
|
18972
|
+
}
|
|
18973
|
+
});
|
|
18974
|
+
|
|
18975
|
+
// dist/content-index-registry.js
|
|
18976
|
+
import path43 from "node:path";
|
|
18977
|
+
function invalidateContentIndexesForRoot2(root) {
|
|
18978
|
+
const rootKey = path43.resolve(root);
|
|
18979
|
+
const prefix = rootKey + path43.sep;
|
|
18980
|
+
for (const [storeRoot, refs] of registry2) {
|
|
18981
|
+
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
18982
|
+
continue;
|
|
18983
|
+
for (const ref of refs) {
|
|
18984
|
+
const target = ref.deref();
|
|
18985
|
+
if (target) {
|
|
18986
|
+
target.invalidateIndex();
|
|
18987
|
+
} else {
|
|
18988
|
+
refs.delete(ref);
|
|
18667
18989
|
}
|
|
18668
18990
|
}
|
|
18669
|
-
|
|
18670
|
-
|
|
18671
|
-
initialValue: false
|
|
18672
|
-
});
|
|
18673
|
-
if (p2.isCancel(confirm5) || !confirm5) {
|
|
18674
|
-
p2.cancel("Pull cancelled. Commit or stash your changes first.");
|
|
18675
|
-
return { fileCount: 0 };
|
|
18676
|
-
}
|
|
18991
|
+
if (refs.size === 0)
|
|
18992
|
+
registry2.delete(storeRoot);
|
|
18677
18993
|
}
|
|
18678
|
-
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18994
|
+
}
|
|
18995
|
+
var registry2, finalization2;
|
|
18996
|
+
var init_content_index_registry = __esm({
|
|
18997
|
+
"dist/content-index-registry.js"() {
|
|
18998
|
+
"use strict";
|
|
18999
|
+
registry2 = /* @__PURE__ */ new Map();
|
|
19000
|
+
finalization2 = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
19001
|
+
const refs = registry2.get(rootKey);
|
|
19002
|
+
if (!refs)
|
|
19003
|
+
return;
|
|
19004
|
+
refs.delete(ref);
|
|
19005
|
+
if (refs.size === 0)
|
|
19006
|
+
registry2.delete(rootKey);
|
|
18684
19007
|
});
|
|
18685
|
-
throw err2;
|
|
18686
|
-
}
|
|
18687
|
-
const postStatus = await sourceGit.status();
|
|
18688
|
-
const contentChanges = postStatus.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
18689
|
-
if (contentChanges.length === 0) {
|
|
18690
|
-
p2.log.info("Content is already up to date \u2014 nothing to pull");
|
|
18691
|
-
} else {
|
|
18692
|
-
p2.log.success(`Pulled ${contentChanges.length} changed file(s) from branch "${branchName}"`);
|
|
18693
|
-
for (const file of contentChanges.slice(0, 20)) {
|
|
18694
|
-
const indicator = file.working_dir === "?" ? "A" : file.working_dir || file.index;
|
|
18695
|
-
p2.log.info(` ${indicator} ${file.path}`);
|
|
18696
|
-
}
|
|
18697
|
-
if (contentChanges.length > 20) {
|
|
18698
|
-
p2.log.info(` ... and ${contentChanges.length - 20} more`);
|
|
18699
|
-
}
|
|
18700
|
-
p2.log.info("\nReview the changes, then git add and commit when ready.");
|
|
18701
19008
|
}
|
|
18702
|
-
|
|
19009
|
+
});
|
|
19010
|
+
|
|
19011
|
+
// dist/id.js
|
|
19012
|
+
import { generate as generate2 } from "short-uuid";
|
|
19013
|
+
function generateId2() {
|
|
19014
|
+
const full = generate2();
|
|
19015
|
+
return full.substring(0, 12);
|
|
18703
19016
|
}
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
|
|
18709
|
-
|
|
18710
|
-
if (!await filePathExists(srcContentDir)) {
|
|
18711
|
-
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
19017
|
+
var isValidId2;
|
|
19018
|
+
var init_id = __esm({
|
|
19019
|
+
"dist/id.js"() {
|
|
19020
|
+
"use strict";
|
|
19021
|
+
init_validation3();
|
|
19022
|
+
isValidId2 = isValidContentId2;
|
|
18712
19023
|
}
|
|
18713
|
-
|
|
18714
|
-
|
|
18715
|
-
|
|
18716
|
-
|
|
18717
|
-
|
|
18718
|
-
|
|
18719
|
-
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
|
|
18726
|
-
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
19024
|
+
});
|
|
19025
|
+
|
|
19026
|
+
// dist/utils/format.js
|
|
19027
|
+
var getFormatExtension2;
|
|
19028
|
+
var init_format = __esm({
|
|
19029
|
+
"dist/utils/format.js"() {
|
|
19030
|
+
"use strict";
|
|
19031
|
+
getFormatExtension2 = (format) => {
|
|
19032
|
+
if (format === "md")
|
|
19033
|
+
return ".md";
|
|
19034
|
+
if (format === "mdx")
|
|
19035
|
+
return ".mdx";
|
|
19036
|
+
if (format === "yaml")
|
|
19037
|
+
return ".yaml";
|
|
19038
|
+
return ".json";
|
|
19039
|
+
};
|
|
19040
|
+
}
|
|
19041
|
+
});
|
|
19042
|
+
|
|
19043
|
+
// dist/content-id-index.js
|
|
19044
|
+
function extractIdFromFilename2(filename) {
|
|
19045
|
+
if (filename.startsWith(".")) {
|
|
19046
|
+
return null;
|
|
19047
|
+
}
|
|
19048
|
+
const parts = filename.split(".");
|
|
19049
|
+
if (parts.length >= 3) {
|
|
19050
|
+
const candidate = parts[parts.length - 2];
|
|
19051
|
+
if (isValidId2(candidate))
|
|
19052
|
+
return candidate;
|
|
19053
|
+
}
|
|
19054
|
+
if (parts.length === 2) {
|
|
19055
|
+
const candidate = parts[parts.length - 1];
|
|
19056
|
+
if (isValidId2(candidate))
|
|
19057
|
+
return candidate;
|
|
19058
|
+
}
|
|
19059
|
+
return null;
|
|
19060
|
+
}
|
|
19061
|
+
function extractSlugFromFilename2(filename, entryTypeName) {
|
|
19062
|
+
const parts = filename.split(".");
|
|
19063
|
+
if (parts.length >= 3) {
|
|
19064
|
+
const possibleId = parts[parts.length - 2];
|
|
19065
|
+
if (isValidId2(possibleId)) {
|
|
19066
|
+
let slugParts = parts.slice(0, parts.length - 2);
|
|
19067
|
+
if (entryTypeName && slugParts.length > 1 && slugParts[0] === entryTypeName) {
|
|
19068
|
+
slugParts = slugParts.slice(1);
|
|
19069
|
+
} else if (parts.length >= 4 && slugParts.length > 1) {
|
|
19070
|
+
slugParts = slugParts.slice(1);
|
|
18731
19071
|
}
|
|
19072
|
+
return slugParts.join(".").toLowerCase();
|
|
18732
19073
|
}
|
|
18733
|
-
return { pushed: 0, pulled: 0 };
|
|
18734
19074
|
}
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
const status = await wsGit.status();
|
|
18741
|
-
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
18742
|
-
await stageAllExceptCanopyState2(wsGit);
|
|
18743
|
-
await wsGit.commit("sync: save editor state before merge");
|
|
18744
|
-
p2.log.info("Committed editor changes before merge");
|
|
19075
|
+
if (parts.length === 2) {
|
|
19076
|
+
const possibleId = parts[parts.length - 1];
|
|
19077
|
+
if (isValidId2(possibleId)) {
|
|
19078
|
+
return parts[0].toLowerCase();
|
|
19079
|
+
}
|
|
18745
19080
|
}
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
await wsGit.raw(["rev-parse", SYNC_BASE_TAG]);
|
|
18749
|
-
baseRef = SYNC_BASE_TAG;
|
|
18750
|
-
} catch {
|
|
18751
|
-
baseRef = "HEAD";
|
|
19081
|
+
if (parts.length > 1) {
|
|
19082
|
+
return parts.slice(0, -1).join(".").toLowerCase();
|
|
18752
19083
|
}
|
|
18753
|
-
|
|
19084
|
+
return filename.toLowerCase();
|
|
19085
|
+
}
|
|
19086
|
+
var ENTRY_EXTENSIONS2;
|
|
19087
|
+
var init_content_id_index = __esm({
|
|
19088
|
+
"dist/content-id-index.js"() {
|
|
19089
|
+
"use strict";
|
|
19090
|
+
init_id();
|
|
19091
|
+
init_format();
|
|
19092
|
+
ENTRY_EXTENSIONS2 = new Set(Object.keys({ md: true, mdx: true, json: true, yaml: true }).map((format) => getFormatExtension2(format)));
|
|
19093
|
+
}
|
|
19094
|
+
});
|
|
19095
|
+
|
|
19096
|
+
// dist/schema/meta-loader.js
|
|
19097
|
+
import { promises as fs35 } from "fs";
|
|
19098
|
+
import { join as join3 } from "pathe";
|
|
19099
|
+
import { z as z18 } from "zod";
|
|
19100
|
+
import chokidar2 from "chokidar";
|
|
19101
|
+
function stripEmbeddedIdFromName2(name) {
|
|
19102
|
+
return extractSlugFromFilename2(name);
|
|
19103
|
+
}
|
|
19104
|
+
async function scanForCollectionMeta2(baseDir, relativePath = "") {
|
|
19105
|
+
const collections = [];
|
|
18754
19106
|
try {
|
|
18755
|
-
const
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
|
|
18765
|
-
});
|
|
18766
|
-
await wsGit.checkout(currentBranch);
|
|
18767
|
-
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
18768
|
-
});
|
|
18769
|
-
throw err2;
|
|
18770
|
-
}
|
|
18771
|
-
await stageAllExceptCanopyState2(wsGit);
|
|
18772
|
-
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
18773
|
-
if (incomingFiles.length === 0) {
|
|
18774
|
-
await wsGit.checkout(currentBranch);
|
|
18775
|
-
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
18776
|
-
p2.log.info("No working-tree changes to merge \u2014 pulling editor changes only");
|
|
18777
|
-
const pullResult2 = await syncPull({ ...options, branch: branchName, force: true });
|
|
18778
|
-
return { pushed: 0, pulled: pullResult2.fileCount };
|
|
18779
|
-
}
|
|
18780
|
-
await wsGit.commit("sync: incoming working-tree changes");
|
|
18781
|
-
await wsGit.checkout(currentBranch);
|
|
18782
|
-
p2.log.step("Merging working-tree changes with editor changes...");
|
|
18783
|
-
try {
|
|
18784
|
-
await wsGit.merge([incomingBranch, "--no-edit"]);
|
|
18785
|
-
} catch (mergeError) {
|
|
18786
|
-
let mergeStatus;
|
|
19107
|
+
const entries = await fs35.readdir(baseDir, { withFileTypes: true });
|
|
19108
|
+
for (const entry of entries) {
|
|
19109
|
+
if (!entry.isDirectory())
|
|
19110
|
+
continue;
|
|
19111
|
+
const folderName = entry.name;
|
|
19112
|
+
const logicalName = stripEmbeddedIdFromName2(folderName);
|
|
19113
|
+
const collectionContentId = extractIdFromFilename2(folderName) ?? void 0;
|
|
19114
|
+
const folderPath = relativePath ? `${relativePath}/${logicalName}` : logicalName;
|
|
19115
|
+
const absolutePath = join3(baseDir, folderName);
|
|
19116
|
+
const metaPath = join3(absolutePath, ".collection.json");
|
|
18787
19117
|
try {
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
19118
|
+
await fs35.access(metaPath);
|
|
19119
|
+
const content = await fs35.readFile(metaPath, "utf-8");
|
|
19120
|
+
const parsed = JSON.parse(content);
|
|
19121
|
+
const meta = collectionMetaSchema2.parse(parsed);
|
|
19122
|
+
collections.push({
|
|
19123
|
+
...meta,
|
|
19124
|
+
path: folderPath,
|
|
19125
|
+
contentId: collectionContentId
|
|
18791
19126
|
});
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
19127
|
+
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
19128
|
+
collections.push(...nestedCollections);
|
|
19129
|
+
} catch (err2) {
|
|
19130
|
+
if (err2.code !== "ENOENT") {
|
|
19131
|
+
canopyLogError2(`Error loading ${metaPath}:`, err2);
|
|
19132
|
+
throw new Error(`Invalid .collection.json in ${folderPath}: ${err2.message}`);
|
|
18798
19133
|
}
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
p2.log.info("Or abort the merge with: canopycms sync abort");
|
|
18802
|
-
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
18803
|
-
});
|
|
18804
|
-
return { pushed: 0, pulled: 0 };
|
|
19134
|
+
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
19135
|
+
collections.push(...nestedCollections);
|
|
18805
19136
|
}
|
|
18806
|
-
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
18807
|
-
});
|
|
18808
|
-
throw mergeError;
|
|
18809
19137
|
}
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
19138
|
+
return collections;
|
|
19139
|
+
} catch (err2) {
|
|
19140
|
+
if (err2.code === "ENOENT") {
|
|
19141
|
+
return [];
|
|
19142
|
+
}
|
|
19143
|
+
throw err2;
|
|
18817
19144
|
}
|
|
18818
19145
|
}
|
|
18819
|
-
async function
|
|
18820
|
-
|
|
18821
|
-
const
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
return;
|
|
19146
|
+
async function loadCollectionMetaFiles2(contentRoot) {
|
|
19147
|
+
let root = null;
|
|
19148
|
+
const rootMetaPath = join3(contentRoot, ".collection.json");
|
|
19149
|
+
try {
|
|
19150
|
+
await fs35.access(rootMetaPath);
|
|
19151
|
+
} catch (err2) {
|
|
19152
|
+
if (err2.code === "ENOENT") {
|
|
19153
|
+
} else {
|
|
19154
|
+
throw err2;
|
|
19155
|
+
}
|
|
18830
19156
|
}
|
|
18831
|
-
|
|
18832
|
-
|
|
18833
|
-
|
|
18834
|
-
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
const result = await syncBoth(options);
|
|
18841
|
-
pushed = result.pushed;
|
|
18842
|
-
pulled = result.pulled;
|
|
18843
|
-
} else if (options.direction === "push") {
|
|
18844
|
-
const result = await syncPush(options);
|
|
18845
|
-
pushed = result.fileCount;
|
|
18846
|
-
} else {
|
|
18847
|
-
const result = await syncPull(options);
|
|
18848
|
-
pulled = result.fileCount;
|
|
19157
|
+
try {
|
|
19158
|
+
const content = await fs35.readFile(rootMetaPath, "utf-8");
|
|
19159
|
+
const parsed = JSON.parse(content);
|
|
19160
|
+
root = rootCollectionMetaSchema2.parse(parsed);
|
|
19161
|
+
} catch (err2) {
|
|
19162
|
+
const errno = err2.code;
|
|
19163
|
+
if (errno !== "ENOENT") {
|
|
19164
|
+
throw new Error(`Invalid root .collection.json`);
|
|
19165
|
+
}
|
|
18849
19166
|
}
|
|
18850
|
-
|
|
18851
|
-
return {
|
|
19167
|
+
const collections = await scanForCollectionMeta2(contentRoot);
|
|
19168
|
+
return { root, collections };
|
|
18852
19169
|
}
|
|
18853
|
-
var
|
|
18854
|
-
var
|
|
18855
|
-
"dist/
|
|
19170
|
+
var entryTypeMetaSchema2, collectionMetaSchema2, rootCollectionMetaSchema2;
|
|
19171
|
+
var init_meta_loader = __esm({
|
|
19172
|
+
"dist/schema/meta-loader.js"() {
|
|
18856
19173
|
"use strict";
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
18865
|
-
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
18869
|
-
|
|
19174
|
+
init_logger3();
|
|
19175
|
+
init_content_id_index();
|
|
19176
|
+
entryTypeMetaSchema2 = z18.object({
|
|
19177
|
+
name: z18.string().min(1),
|
|
19178
|
+
format: z18.enum(["md", "mdx", "json", "yaml"]),
|
|
19179
|
+
schema: z18.string().min(1),
|
|
19180
|
+
// Entry schema registry key (validated at resolution time)
|
|
19181
|
+
label: z18.string().optional(),
|
|
19182
|
+
description: z18.string().optional(),
|
|
19183
|
+
default: z18.boolean().optional(),
|
|
19184
|
+
maxItems: z18.number().int().positive().optional()
|
|
19185
|
+
});
|
|
19186
|
+
collectionMetaSchema2 = z18.object({
|
|
19187
|
+
name: z18.string().min(1),
|
|
19188
|
+
label: z18.string().optional(),
|
|
19189
|
+
description: z18.string().optional(),
|
|
19190
|
+
entries: z18.array(entryTypeMetaSchema2).optional(),
|
|
19191
|
+
order: z18.array(z18.string()).default([])
|
|
19192
|
+
// Embedded IDs for ordering; optional in the file, [] = alphabetical
|
|
19193
|
+
}).refine((data) => data.entries && data.entries.length > 0, {
|
|
19194
|
+
message: "Collection must have at least one entry type"
|
|
19195
|
+
});
|
|
19196
|
+
rootCollectionMetaSchema2 = z18.object({
|
|
19197
|
+
label: z18.string().optional(),
|
|
19198
|
+
entries: z18.array(entryTypeMetaSchema2).optional(),
|
|
19199
|
+
order: z18.array(z18.string()).optional()
|
|
19200
|
+
// Embedded IDs for ordering items
|
|
19201
|
+
});
|
|
18870
19202
|
}
|
|
18871
19203
|
});
|
|
18872
19204
|
|
|
18873
|
-
// dist/
|
|
18874
|
-
|
|
18875
|
-
|
|
19205
|
+
// dist/branch-schema-cache.js
|
|
19206
|
+
import { LRUCache as LRUCache2 } from "lru-cache";
|
|
19207
|
+
var SNAPSHOT_MEMO_MAX_BYTES2, SCHEMA_GENERATION_RESOURCE2;
|
|
19208
|
+
var init_branch_schema_cache = __esm({
|
|
19209
|
+
"dist/branch-schema-cache.js"() {
|
|
18876
19210
|
"use strict";
|
|
18877
|
-
|
|
19211
|
+
SNAPSHOT_MEMO_MAX_BYTES2 = 64 * 1024 * 1024;
|
|
19212
|
+
SCHEMA_GENERATION_RESOURCE2 = "schema";
|
|
18878
19213
|
}
|
|
18879
19214
|
});
|
|
18880
19215
|
|
|
18881
|
-
// dist/
|
|
18882
|
-
|
|
18883
|
-
|
|
19216
|
+
// dist/content-index-generation.js
|
|
19217
|
+
async function bumpContentIndexGeneration2(root) {
|
|
19218
|
+
return bumpResourceGeneration2(root, RESOURCE4);
|
|
19219
|
+
}
|
|
19220
|
+
async function invalidateBranchContentCaches2(root) {
|
|
19221
|
+
await bumpContentIndexGeneration2(root);
|
|
19222
|
+
await bumpResourceGeneration2(root, SCHEMA_GENERATION_RESOURCE2);
|
|
19223
|
+
invalidateContentIndexesForRoot2(root);
|
|
19224
|
+
}
|
|
19225
|
+
var RESOURCE4;
|
|
19226
|
+
var init_content_index_generation = __esm({
|
|
19227
|
+
"dist/content-index-generation.js"() {
|
|
18884
19228
|
"use strict";
|
|
18885
|
-
|
|
19229
|
+
init_resource_generation();
|
|
19230
|
+
init_content_index_registry();
|
|
19231
|
+
init_branch_schema_cache();
|
|
19232
|
+
RESOURCE4 = "content-index";
|
|
18886
19233
|
}
|
|
18887
19234
|
});
|
|
18888
19235
|
|
|
18889
|
-
// dist/utils/
|
|
18890
|
-
import
|
|
18891
|
-
import
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
const
|
|
18900
|
-
|
|
18901
|
-
await fs35.mkdir(dir, { recursive: true });
|
|
18902
|
-
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
18903
|
-
await fs35.writeFile(tempPath, content, "utf-8");
|
|
18904
|
-
if (expectedVersion === null) {
|
|
19236
|
+
// dist/utils/content-write-lock.js
|
|
19237
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
19238
|
+
import path44 from "node:path";
|
|
19239
|
+
function lockTargetDir2(branchRoot) {
|
|
19240
|
+
return path44.join(path44.resolve(branchRoot), META_DIR4);
|
|
19241
|
+
}
|
|
19242
|
+
function tryAcquireContentWriteLock2(branchRoot, onCompromised) {
|
|
19243
|
+
return tryAcquireProvisioningLock2(lockTargetDir2(branchRoot), CONTENT_WRITE_LOCK_NAME2, onCompromised, CONTENT_WRITE_LOCK_STALE_MS2, false);
|
|
19244
|
+
}
|
|
19245
|
+
async function acquireContentWriteLock2(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2, onCompromised) {
|
|
19246
|
+
const startedAt = Date.now();
|
|
19247
|
+
for (let attempt = 0; ; attempt++) {
|
|
18905
19248
|
try {
|
|
18906
|
-
await
|
|
19249
|
+
return await tryAcquireContentWriteLock2(branchRoot, onCompromised);
|
|
18907
19250
|
} catch (err2) {
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
19251
|
+
if (!isNodeError(err2) || err2.code !== "ELOCKED")
|
|
19252
|
+
throw err2;
|
|
19253
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
19254
|
+
if (remaining <= 0)
|
|
19255
|
+
throw new ContentWriteLockBusyError2();
|
|
19256
|
+
const base = Math.min(40 * 2 ** attempt, 250);
|
|
19257
|
+
await sleep3(Math.min(base * (0.5 + Math.random()), remaining));
|
|
18914
19258
|
}
|
|
18915
|
-
await fs35.unlink(tempPath).catch(() => {
|
|
18916
|
-
});
|
|
18917
|
-
return { version: newVersion, writeId };
|
|
18918
19259
|
}
|
|
19260
|
+
}
|
|
19261
|
+
async function withContentWriteLock2(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
19262
|
+
let compromised = false;
|
|
19263
|
+
const release = await acquireContentWriteLock2(branchRoot, waitMs, (err2) => {
|
|
19264
|
+
compromised = true;
|
|
19265
|
+
canopyLogWarn3(`[canopy] Content-write lock compromised mid-hold for ${branchRoot}:`, getErrorMessage(err2));
|
|
19266
|
+
});
|
|
19267
|
+
let result;
|
|
18919
19268
|
try {
|
|
18920
|
-
|
|
18921
|
-
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
currentVersion = null;
|
|
18926
|
-
}
|
|
18927
|
-
if (currentVersion !== expectedVersion) {
|
|
18928
|
-
throw new OccWriteConflictError2();
|
|
18929
|
-
}
|
|
18930
|
-
await fs35.rename(tempPath, filePath);
|
|
18931
|
-
if (settleMs > 0) {
|
|
18932
|
-
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
18933
|
-
}
|
|
18934
|
-
const afterWrite = JSON.parse(await fs35.readFile(filePath, "utf-8"));
|
|
18935
|
-
if (afterWrite.writeId !== writeId) {
|
|
18936
|
-
throw new OccWriteConflictError2();
|
|
18937
|
-
}
|
|
18938
|
-
} catch (err2) {
|
|
18939
|
-
await fs35.unlink(tempPath).catch(() => {
|
|
18940
|
-
});
|
|
18941
|
-
throw err2;
|
|
19269
|
+
const held = new Set(heldRoots2.getStore());
|
|
19270
|
+
held.add(path44.resolve(branchRoot));
|
|
19271
|
+
result = await heldRoots2.run(held, fn);
|
|
19272
|
+
} finally {
|
|
19273
|
+
await release();
|
|
18942
19274
|
}
|
|
18943
|
-
|
|
19275
|
+
if (compromised) {
|
|
19276
|
+
throw new ContentWriteLockBusyError2("We couldn't confirm your change was saved because the branch was syncing at the same time. Reload to check before saving again (your unsaved edits will be lost).", "unknown");
|
|
19277
|
+
}
|
|
19278
|
+
return result;
|
|
18944
19279
|
}
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
19280
|
+
var META_DIR4, CONTENT_WRITE_LOCK_NAME2, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2, ContentWriteLockBusyError2, sleep3, CONTENT_WRITE_LOCK_STALE_MS2, heldRoots2;
|
|
19281
|
+
var init_content_write_lock = __esm({
|
|
19282
|
+
"dist/utils/content-write-lock.js"() {
|
|
19283
|
+
"use strict";
|
|
19284
|
+
init_error();
|
|
19285
|
+
init_logger3();
|
|
19286
|
+
init_provisioning_lock();
|
|
19287
|
+
META_DIR4 = ".canopy-meta";
|
|
19288
|
+
CONTENT_WRITE_LOCK_NAME2 = "content-write.lock";
|
|
19289
|
+
DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2 = 2e3;
|
|
19290
|
+
ContentWriteLockBusyError2 = class extends Error {
|
|
19291
|
+
constructor(message = "This branch is busy syncing or saving another change, so your change was not saved. Try again in a moment.", outcome = "not-run") {
|
|
19292
|
+
super(message);
|
|
19293
|
+
this.outcome = outcome;
|
|
19294
|
+
this.name = "ContentWriteLockBusyError";
|
|
18958
19295
|
}
|
|
18959
|
-
|
|
18960
|
-
|
|
19296
|
+
};
|
|
19297
|
+
sleep3 = (ms) => new Promise((resolve2) => {
|
|
19298
|
+
setTimeout(resolve2, ms);
|
|
19299
|
+
});
|
|
19300
|
+
CONTENT_WRITE_LOCK_STALE_MS2 = 3e4;
|
|
19301
|
+
heldRoots2 = new AsyncLocalStorage3();
|
|
19302
|
+
}
|
|
19303
|
+
});
|
|
19304
|
+
|
|
19305
|
+
// dist/sync-core.js
|
|
19306
|
+
import fs36 from "node:fs/promises";
|
|
19307
|
+
import path45 from "node:path";
|
|
19308
|
+
import { simpleGit as simpleGit5 } from "simple-git";
|
|
19309
|
+
function assertWithinDir(resolved, parent, label) {
|
|
19310
|
+
const normalizedResolved = path45.resolve(resolved);
|
|
19311
|
+
const normalizedParent = path45.resolve(parent);
|
|
19312
|
+
if (!normalizedResolved.startsWith(normalizedParent + path45.sep) && normalizedResolved !== normalizedParent) {
|
|
19313
|
+
throw new Error(`${label} escapes the expected directory: ${resolved}`);
|
|
18961
19314
|
}
|
|
18962
|
-
throw new Error("Unreachable");
|
|
18963
19315
|
}
|
|
18964
|
-
async function
|
|
19316
|
+
async function safeReplaceDir(oldDir, newDir) {
|
|
19317
|
+
const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
19318
|
+
const oldExists = await filePathExists(oldDir);
|
|
19319
|
+
if (oldExists) {
|
|
19320
|
+
await fs36.rename(oldDir, backupDir);
|
|
19321
|
+
}
|
|
18965
19322
|
try {
|
|
18966
|
-
await
|
|
19323
|
+
await fs36.rename(newDir, oldDir);
|
|
18967
19324
|
} catch (err2) {
|
|
18968
|
-
if (
|
|
18969
|
-
|
|
18970
|
-
}
|
|
18971
|
-
}
|
|
18972
|
-
const MAX_ATTEMPTS = 14;
|
|
18973
|
-
let release = null;
|
|
18974
|
-
for (let attempt = 1; release === null; attempt++) {
|
|
18975
|
-
try {
|
|
18976
|
-
release = await lockfile4.lock(filePath, {
|
|
18977
|
-
lockfilePath: `${filePath}.lock`,
|
|
18978
|
-
realpath: false,
|
|
18979
|
-
stale: 1e4,
|
|
18980
|
-
retries: 0,
|
|
18981
|
-
// A compromised lock (the lock dir vanished or refresh failed mid-hold, e.g. the branch
|
|
18982
|
-
// directory containing it was deleted, or an EFS waiter misread a cached mtime and took
|
|
18983
|
-
// it over) must not crash the process, which is proper-lockfile's default. Our critical
|
|
18984
|
-
// sections are short and idempotent-on-conflict (OCC verify inside); log and let the
|
|
18985
|
-
// section finish.
|
|
18986
|
-
onCompromised: guardOnCompromised2((err2) => canopyLogWarn3(`[canopy] OCC file lock compromised mid-hold for ${filePath}:`, getErrorMessage(err2)), filePath)
|
|
19325
|
+
if (oldExists) {
|
|
19326
|
+
await fs36.rename(backupDir, oldDir).catch(() => {
|
|
18987
19327
|
});
|
|
18988
|
-
} catch (err2) {
|
|
18989
|
-
const code = isNodeError(err2) ? err2.code : void 0;
|
|
18990
|
-
const contended = code === "ELOCKED";
|
|
18991
|
-
if (!contended || attempt >= MAX_ATTEMPTS) {
|
|
18992
|
-
throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err2)}`);
|
|
18993
|
-
}
|
|
18994
|
-
const baseDelay = Math.min(50 * Math.pow(1.6, attempt - 1), 2e3);
|
|
18995
|
-
const jitter = Math.random() * baseDelay;
|
|
18996
|
-
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
18997
19328
|
}
|
|
19329
|
+
throw err2;
|
|
18998
19330
|
}
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
} finally {
|
|
19002
|
-
await release().catch((err2) => {
|
|
19003
|
-
if (isNodeError(err2) && err2.code === "ENOENT")
|
|
19004
|
-
return;
|
|
19005
|
-
log14.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err2) });
|
|
19331
|
+
if (oldExists) {
|
|
19332
|
+
await fs36.rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
19006
19333
|
});
|
|
19007
19334
|
}
|
|
19008
19335
|
}
|
|
19009
|
-
|
|
19010
|
-
|
|
19011
|
-
|
|
19012
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
init_provisioning_lock();
|
|
19017
|
-
log14 = createDebugLogger3({ prefix: "OccJsonWrite" });
|
|
19018
|
-
OccWriteConflictError2 = class extends Error {
|
|
19019
|
-
constructor(message = "Concurrent modification detected") {
|
|
19020
|
-
super(message);
|
|
19021
|
-
this.name = "OccWriteConflictError";
|
|
19022
|
-
}
|
|
19023
|
-
};
|
|
19336
|
+
async function listFilesRecursive(dir, prefix = "") {
|
|
19337
|
+
const results = [];
|
|
19338
|
+
let entries;
|
|
19339
|
+
try {
|
|
19340
|
+
entries = await fs36.readdir(dir, { withFileTypes: true });
|
|
19341
|
+
} catch {
|
|
19342
|
+
return results;
|
|
19024
19343
|
}
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19344
|
+
for (const entry of entries) {
|
|
19345
|
+
if (entry.name === ".git")
|
|
19346
|
+
continue;
|
|
19347
|
+
if (entry.isSymbolicLink())
|
|
19348
|
+
continue;
|
|
19349
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
19350
|
+
if (entry.isDirectory()) {
|
|
19351
|
+
results.push(...await listFilesRecursive(path45.join(dir, entry.name), rel));
|
|
19352
|
+
} else {
|
|
19353
|
+
results.push(rel);
|
|
19354
|
+
}
|
|
19355
|
+
}
|
|
19356
|
+
return results;
|
|
19357
|
+
}
|
|
19358
|
+
async function copyDir(src, dest) {
|
|
19359
|
+
await fs36.mkdir(dest, { recursive: true });
|
|
19360
|
+
const entries = await fs36.readdir(src, { withFileTypes: true });
|
|
19361
|
+
for (const entry of entries) {
|
|
19362
|
+
if (entry.name === ".git")
|
|
19363
|
+
continue;
|
|
19364
|
+
if (entry.isSymbolicLink())
|
|
19365
|
+
continue;
|
|
19366
|
+
const srcPath = path45.join(src, entry.name);
|
|
19367
|
+
const destPath = path45.join(dest, entry.name);
|
|
19368
|
+
if (entry.isDirectory()) {
|
|
19369
|
+
await copyDir(srcPath, destPath);
|
|
19370
|
+
} else {
|
|
19371
|
+
await fs36.copyFile(srcPath, destPath);
|
|
19372
|
+
}
|
|
19373
|
+
}
|
|
19374
|
+
}
|
|
19375
|
+
async function pushContentToWorkspace(options) {
|
|
19376
|
+
const { srcContentDir, branchPath, contentRoot } = options;
|
|
19377
|
+
if (!await filePathExists(srcContentDir)) {
|
|
19378
|
+
return { fileCount: 0, savedEditorState: false };
|
|
19379
|
+
}
|
|
19380
|
+
const wsContentDir = path45.join(branchPath, contentRoot);
|
|
19381
|
+
assertWithinDir(wsContentDir, branchPath, "content-root");
|
|
19382
|
+
return withContentWriteLock2(branchPath, () => replaceWorkspaceContent(options, wsContentDir));
|
|
19383
|
+
}
|
|
19384
|
+
async function replaceWorkspaceContent(options, wsContentDir) {
|
|
19385
|
+
const { srcContentDir, branchPath, commitMessage, baseTag, saveEditorStateMessage } = options;
|
|
19386
|
+
const wsGit = simpleGit5({ baseDir: branchPath });
|
|
19387
|
+
let savedEditorState = false;
|
|
19388
|
+
if (saveEditorStateMessage) {
|
|
19389
|
+
const editorChanges = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19390
|
+
if (editorChanges.length > 0) {
|
|
19391
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19392
|
+
await wsGit.commit(saveEditorStateMessage);
|
|
19393
|
+
savedEditorState = true;
|
|
19394
|
+
}
|
|
19395
|
+
}
|
|
19396
|
+
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
19036
19397
|
try {
|
|
19037
|
-
|
|
19398
|
+
await copyDir(srcContentDir, tmpDir);
|
|
19399
|
+
await safeReplaceDir(wsContentDir, tmpDir);
|
|
19400
|
+
} catch (err2) {
|
|
19401
|
+
await fs36.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
19402
|
+
});
|
|
19403
|
+
throw err2;
|
|
19404
|
+
}
|
|
19405
|
+
try {
|
|
19406
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19407
|
+
const staged = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19408
|
+
if (staged.length === 0) {
|
|
19409
|
+
if (baseTag)
|
|
19410
|
+
await wsGit.tag(["-f", baseTag]);
|
|
19411
|
+
return { fileCount: 0, savedEditorState };
|
|
19412
|
+
}
|
|
19413
|
+
await wsGit.commit(commitMessage ?? "sync: update content from working tree");
|
|
19414
|
+
if (baseTag)
|
|
19415
|
+
await wsGit.tag(["-f", baseTag]);
|
|
19416
|
+
return { fileCount: staged.length, savedEditorState };
|
|
19038
19417
|
} finally {
|
|
19039
|
-
|
|
19040
|
-
if (locks2.get(key) === next)
|
|
19041
|
-
locks2.delete(key);
|
|
19418
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
19042
19419
|
}
|
|
19043
19420
|
}
|
|
19044
|
-
var
|
|
19045
|
-
var
|
|
19046
|
-
"dist/
|
|
19421
|
+
var SYNC_BASE_TAG;
|
|
19422
|
+
var init_sync_core = __esm({
|
|
19423
|
+
"dist/sync-core.js"() {
|
|
19047
19424
|
"use strict";
|
|
19048
|
-
|
|
19425
|
+
init_content_index_generation();
|
|
19426
|
+
init_content_write_lock();
|
|
19427
|
+
init_fs();
|
|
19428
|
+
init_git();
|
|
19429
|
+
SYNC_BASE_TAG = "canopycms-sync-base";
|
|
19049
19430
|
}
|
|
19050
19431
|
});
|
|
19051
19432
|
|
|
19052
|
-
// dist/
|
|
19053
|
-
import
|
|
19054
|
-
import
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19433
|
+
// dist/git-manager.js
|
|
19434
|
+
import fs37 from "node:fs/promises";
|
|
19435
|
+
import path46 from "node:path";
|
|
19436
|
+
import { simpleGit as simpleGit6 } from "simple-git";
|
|
19437
|
+
async function ensureGitExcludePattern2(repoPath, pattern) {
|
|
19438
|
+
const excludePath = path46.join(repoPath, ".git", "info", "exclude");
|
|
19439
|
+
await fs37.mkdir(path46.dirname(excludePath), { recursive: true });
|
|
19440
|
+
let content = "";
|
|
19441
|
+
try {
|
|
19442
|
+
content = await fs37.readFile(excludePath, "utf-8");
|
|
19443
|
+
} catch (err2) {
|
|
19444
|
+
if (!isNotFoundError(err2))
|
|
19445
|
+
throw err2;
|
|
19446
|
+
}
|
|
19447
|
+
const lines = content.split("\n");
|
|
19448
|
+
if (lines.some((line) => line.trim() === pattern)) {
|
|
19449
|
+
log14.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
19450
|
+
return;
|
|
19451
|
+
}
|
|
19452
|
+
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
19453
|
+
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
19454
|
+
await fs37.writeFile(excludePath, newContent, "utf-8");
|
|
19455
|
+
log14.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
19058
19456
|
}
|
|
19059
|
-
var
|
|
19060
|
-
var
|
|
19061
|
-
"dist/
|
|
19457
|
+
var log14;
|
|
19458
|
+
var init_git_manager = __esm({
|
|
19459
|
+
"dist/git-manager.js"() {
|
|
19062
19460
|
"use strict";
|
|
19063
|
-
init_async_mutex();
|
|
19064
19461
|
init_debug2();
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
MAX_NAME_LENGTH = 64;
|
|
19068
|
-
MAX_LABEL_LENGTH = 128;
|
|
19069
|
-
SAFE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
19070
|
-
SAFE_NAME_MESSAGE = "must start with a letter and contain only lowercase letters, numbers, and hyphens";
|
|
19071
|
-
entryTypeInputSchema = z17.object({
|
|
19072
|
-
name: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Entry type name ${SAFE_NAME_MESSAGE}`),
|
|
19073
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19074
|
-
format: z17.enum(["md", "mdx", "json", "yaml"]),
|
|
19075
|
-
schema: z17.string().min(1),
|
|
19076
|
-
default: z17.boolean().optional(),
|
|
19077
|
-
maxItems: z17.number().int().positive().optional()
|
|
19078
|
-
});
|
|
19079
|
-
createCollectionInputSchema = z17.object({
|
|
19080
|
-
name: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`),
|
|
19081
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19082
|
-
parentPath: z17.string().optional(),
|
|
19083
|
-
entries: z17.array(entryTypeInputSchema).min(1, "Collection must have at least one entry type")
|
|
19084
|
-
});
|
|
19085
|
-
updateCollectionInputSchema = z17.object({
|
|
19086
|
-
name: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`).optional(),
|
|
19087
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19088
|
-
// Directory name (e.g., "posts" in "posts.{id}/")
|
|
19089
|
-
slug: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Slug ${SAFE_NAME_MESSAGE}`).optional(),
|
|
19090
|
-
order: z17.array(z17.string()).optional()
|
|
19091
|
-
});
|
|
19092
|
-
updateEntryTypeInputSchema = z17.object({
|
|
19093
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19094
|
-
format: z17.enum(["md", "mdx", "json", "yaml"]).optional(),
|
|
19095
|
-
schema: z17.string().min(1).optional(),
|
|
19096
|
-
default: z17.boolean().optional(),
|
|
19097
|
-
maxItems: z17.number().int().positive().optional()
|
|
19098
|
-
});
|
|
19099
|
-
log15 = createDebugLogger3({ prefix: "SchemaOps" });
|
|
19462
|
+
init_error();
|
|
19463
|
+
log14 = createDebugLogger3({ prefix: "GitManager" });
|
|
19100
19464
|
}
|
|
19101
19465
|
});
|
|
19102
19466
|
|
|
19103
|
-
// dist/
|
|
19104
|
-
var
|
|
19105
|
-
|
|
19106
|
-
|
|
19107
|
-
"use strict";
|
|
19108
|
-
BranchMetadataCorruptError2 = class extends Error {
|
|
19109
|
-
constructor(branchRoot, cause) {
|
|
19110
|
-
super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
|
|
19111
|
-
this.name = "BranchMetadataCorruptError";
|
|
19112
|
-
this.branchRoot = branchRoot;
|
|
19113
|
-
this.parseCause = cause;
|
|
19114
|
-
}
|
|
19115
|
-
};
|
|
19116
|
-
}
|
|
19467
|
+
// dist/cli/sync.js
|
|
19468
|
+
var sync_exports = {};
|
|
19469
|
+
__export(sync_exports, {
|
|
19470
|
+
sync: () => sync
|
|
19117
19471
|
});
|
|
19118
|
-
|
|
19119
|
-
|
|
19120
|
-
import
|
|
19121
|
-
import
|
|
19122
|
-
import { z as
|
|
19123
|
-
function
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
|
|
19472
|
+
import fs38 from "node:fs/promises";
|
|
19473
|
+
import path47 from "node:path";
|
|
19474
|
+
import { simpleGit as simpleGit7 } from "simple-git";
|
|
19475
|
+
import * as p2 from "@clack/prompts";
|
|
19476
|
+
import { z as z19 } from "zod";
|
|
19477
|
+
function lockBusyAsSyncError(err2, branchName) {
|
|
19478
|
+
if (!(err2 instanceof ContentWriteLockBusyError2))
|
|
19479
|
+
return err2;
|
|
19480
|
+
return new SyncError(err2.outcome === "not-run" ? `Branch workspace "${branchName}" is busy (the worker is syncing it, or an editor save is in flight); nothing was changed. Try again in a moment.` : `Branch workspace "${branchName}" was being synced during this run, so it may be partly updated. Check its state before running sync again.`);
|
|
19481
|
+
}
|
|
19482
|
+
async function withWorkspaceLock(branchPath, branchName, fn) {
|
|
19483
|
+
try {
|
|
19484
|
+
return await withContentWriteLock2(branchPath, fn);
|
|
19485
|
+
} catch (err2) {
|
|
19486
|
+
throw lockBusyAsSyncError(err2, branchName);
|
|
19130
19487
|
}
|
|
19131
|
-
const parts = [];
|
|
19132
|
-
if (missing.size > 0)
|
|
19133
|
-
parts.push(`Missing: ${[...missing].join(", ")}`);
|
|
19134
|
-
if (invalid.size > 0)
|
|
19135
|
-
parts.push(`Invalid: ${[...invalid].join(", ")}`);
|
|
19136
|
-
return `Not branch metadata. ${parts.join(". ")}`;
|
|
19137
19488
|
}
|
|
19138
|
-
async function
|
|
19139
|
-
|
|
19140
|
-
|
|
19489
|
+
async function abortWorkspaceMerge(branchPath, branchName) {
|
|
19490
|
+
return withWorkspaceLock(branchPath, branchName, async () => {
|
|
19491
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19492
|
+
if ((await wsGit.status()).conflicted.length === 0)
|
|
19493
|
+
return false;
|
|
19494
|
+
await wsGit.merge(["--abort"]);
|
|
19495
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
19496
|
+
return true;
|
|
19497
|
+
});
|
|
19498
|
+
}
|
|
19499
|
+
async function detectCurrentBranch(projectDir) {
|
|
19500
|
+
const git = simpleGit7({ baseDir: projectDir });
|
|
19501
|
+
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
19502
|
+
if (branch === "HEAD")
|
|
19503
|
+
throw new Error("Detached HEAD \u2014 use --branch to specify a branch name");
|
|
19504
|
+
return branch;
|
|
19505
|
+
}
|
|
19506
|
+
async function readBranchConfigFields(projectDir) {
|
|
19507
|
+
if (!await filePathExists(path47.join(projectDir, "canopycms.config.ts")))
|
|
19508
|
+
return {};
|
|
19509
|
+
let why;
|
|
19141
19510
|
try {
|
|
19142
|
-
|
|
19511
|
+
const parsed = branchConfigSchema.safeParse((await importSiteServerConfig(projectDir)).server);
|
|
19512
|
+
if (parsed.success)
|
|
19513
|
+
return parsed.data;
|
|
19514
|
+
why = parsed.error.message;
|
|
19143
19515
|
} catch (err2) {
|
|
19144
|
-
|
|
19516
|
+
why = getErrorMessage(err2);
|
|
19517
|
+
}
|
|
19518
|
+
p2.log.warn(`Could not use canopycms.config.ts (${why}); branch names resolve against the default branch settings.`);
|
|
19519
|
+
return {};
|
|
19520
|
+
}
|
|
19521
|
+
async function loadBranchParamConfig(projectDir) {
|
|
19522
|
+
const fields = await readBranchConfigFields(projectDir);
|
|
19523
|
+
const head = await detectHeadBranch2(projectDir, fields.defaultBaseBranch ?? "main");
|
|
19524
|
+
return {
|
|
19525
|
+
...fields,
|
|
19526
|
+
mode: "dev",
|
|
19527
|
+
defaultBaseBranch: fields.defaultBaseBranch ?? head,
|
|
19528
|
+
defaultActiveBranch: fields.defaultActiveBranch ?? head
|
|
19529
|
+
};
|
|
19530
|
+
}
|
|
19531
|
+
async function assertOwnRepository(branchPath) {
|
|
19532
|
+
const top = await simpleGit7({ baseDir: branchPath }).revparse(["--show-toplevel"]).then((out) => out.trim()).catch(() => "");
|
|
19533
|
+
if (top && await fs38.realpath(top) === await fs38.realpath(branchPath))
|
|
19534
|
+
return;
|
|
19535
|
+
throw new SyncError(`"${path47.basename(branchPath)}" under content-branches/ is not a branch workspace: git does not report it as the top of its own repository.`);
|
|
19536
|
+
}
|
|
19537
|
+
async function workspaceHead(branchPath) {
|
|
19538
|
+
return simpleGit7({ baseDir: branchPath }).raw(["symbolic-ref", "--short", "-q", "HEAD"]).then((out) => out.trim() || null).catch(() => null);
|
|
19539
|
+
}
|
|
19540
|
+
async function keyWorkspace(key, projectDir) {
|
|
19541
|
+
const branchPath = path47.join(operatingStrategy3("dev").getContentBranchesRoot(projectDir), key);
|
|
19542
|
+
await assertOwnRepository(branchPath);
|
|
19543
|
+
const meta = await readBranchMetadataFile2(branchPath);
|
|
19544
|
+
return { branchName: meta?.branch.name ?? await workspaceHead(branchPath) ?? key, branchPath };
|
|
19545
|
+
}
|
|
19546
|
+
function refOf(name) {
|
|
19547
|
+
if (!isExactBranchForm(name))
|
|
19548
|
+
return name;
|
|
19549
|
+
const ref = exactBranchName(name);
|
|
19550
|
+
if (ref === null)
|
|
19551
|
+
throw new SyncError(`"${name}" is not a valid branch name.`);
|
|
19552
|
+
return ref;
|
|
19553
|
+
}
|
|
19554
|
+
async function resolveSyncWorkspace(name, projectDir, keys, byRef) {
|
|
19555
|
+
const config = await loadBranchParamConfig(projectDir);
|
|
19556
|
+
const param = byRef ? exactBranchSegment(name) : name;
|
|
19557
|
+
const resolved = await resolveBranchParam(param, config, { basePathOverride: projectDir }).catch((err2) => {
|
|
19558
|
+
if (namesNoWorkspace(err2))
|
|
19145
19559
|
return null;
|
|
19146
|
-
|
|
19560
|
+
throw err2;
|
|
19561
|
+
});
|
|
19562
|
+
if (resolved) {
|
|
19563
|
+
const ref2 = refOf(name);
|
|
19564
|
+
if (resolved.branch.name !== ref2 && resolved.branchRoot === refKeyPath(ref2, projectDir)) {
|
|
19565
|
+
throw holdsAnotherBranch(resolved.branchRoot, resolved.branch.name, ref2);
|
|
19566
|
+
}
|
|
19567
|
+
await assertOwnRepository(resolved.branchRoot);
|
|
19568
|
+
return { branchName: resolved.branch.name, branchPath: resolved.branchRoot };
|
|
19569
|
+
}
|
|
19570
|
+
if (!byRef && keys.includes(name))
|
|
19571
|
+
return keyWorkspace(name, projectDir);
|
|
19572
|
+
const ref = refOf(name);
|
|
19573
|
+
const branchPath = refKeyPath(ref, projectDir);
|
|
19574
|
+
const present = await filePathExists(branchPath).catch((err2) => {
|
|
19575
|
+
if (namesNoWorkspace(err2))
|
|
19576
|
+
throw new SyncError(`"${ref}" cannot name a branch workspace.`);
|
|
19577
|
+
throw err2;
|
|
19578
|
+
});
|
|
19579
|
+
if (!present)
|
|
19580
|
+
return null;
|
|
19581
|
+
await assertOwnRepository(branchPath);
|
|
19582
|
+
const meta = await readBranchMetadataFile2(branchPath);
|
|
19583
|
+
const holder = meta ? meta.branch.name : await workspaceHead(branchPath);
|
|
19584
|
+
if (holder !== ref)
|
|
19585
|
+
throw holdsAnotherBranch(branchPath, holder, ref);
|
|
19586
|
+
return { branchName: ref, branchPath };
|
|
19587
|
+
}
|
|
19588
|
+
function holdsAnotherBranch(branchPath, holder, ref) {
|
|
19589
|
+
const recovery = holder?.startsWith("sync-incoming-") ? ` An interrupted "sync both" left it there: run "git checkout ${ref}" in that workspace.` : "";
|
|
19590
|
+
return new SyncError(`Branch workspace "${path47.basename(branchPath)}" holds ${holder === null ? "a detached HEAD" : `branch "${holder}"`}, not "${ref}".${recovery}`);
|
|
19591
|
+
}
|
|
19592
|
+
function refKeyPath(name, projectDir) {
|
|
19593
|
+
try {
|
|
19594
|
+
return resolveBranchPath2({ mode: "dev", branchName: name, basePathOverride: projectDir }).branchRoot;
|
|
19595
|
+
} catch (err2) {
|
|
19596
|
+
if (namesNoWorkspace(err2))
|
|
19597
|
+
throw new SyncError(`"${name}" cannot name a branch workspace.`);
|
|
19147
19598
|
throw err2;
|
|
19148
19599
|
}
|
|
19149
|
-
return parseBranchMetadataFile2(resolvedRoot, raw);
|
|
19150
19600
|
}
|
|
19151
|
-
function
|
|
19152
|
-
|
|
19601
|
+
async function createSyncWorkspace(name, projectDir) {
|
|
19602
|
+
const ref = refOf(name);
|
|
19603
|
+
const parsed = parseBranchRef3(ref);
|
|
19604
|
+
if (!parsed.ok)
|
|
19605
|
+
throw new SyncError(`"${ref}" is not a valid branch name: ${parsed.error}`);
|
|
19606
|
+
const branchPath = refKeyPath(ref, projectDir);
|
|
19607
|
+
await fs38.mkdir(branchPath, { recursive: true });
|
|
19608
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19609
|
+
await wsGit.init();
|
|
19610
|
+
await wsGit.checkoutLocalBranch(ref);
|
|
19611
|
+
await wsGit.raw(["commit", "--allow-empty", "-m", "init: workspace created by sync"]);
|
|
19612
|
+
await ensureGitExcludePattern2(branchPath, operatingStrategy3("dev").getGitExcludePattern());
|
|
19613
|
+
p2.log.info(`Created branch workspace: ${path47.basename(branchPath)}`);
|
|
19614
|
+
return { branchName: ref, branchPath };
|
|
19615
|
+
}
|
|
19616
|
+
async function selectBranch(options) {
|
|
19617
|
+
const branchesDir = operatingStrategy3("dev").getContentBranchesRoot(options.projectDir);
|
|
19618
|
+
let branches = [];
|
|
19619
|
+
if (await filePathExists(branchesDir)) {
|
|
19620
|
+
const entries = await fs38.readdir(branchesDir, { withFileTypes: true });
|
|
19621
|
+
branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
19622
|
+
}
|
|
19623
|
+
let branchName = options.branch;
|
|
19624
|
+
const byRef = !branchName;
|
|
19625
|
+
if (!branchName) {
|
|
19626
|
+
try {
|
|
19627
|
+
branchName = await detectCurrentBranch(options.projectDir);
|
|
19628
|
+
} catch {
|
|
19629
|
+
}
|
|
19630
|
+
}
|
|
19631
|
+
if (!branchName) {
|
|
19632
|
+
if (branches.length === 0) {
|
|
19633
|
+
throw new SyncError("No branch workspaces found and could not detect current git branch. Use --branch to specify.");
|
|
19634
|
+
}
|
|
19635
|
+
if (branches.length === 1)
|
|
19636
|
+
return keyWorkspace(branches[0], options.projectDir);
|
|
19637
|
+
const result = await p2.select({
|
|
19638
|
+
message: "Which branch?",
|
|
19639
|
+
options: branches.map((b) => ({ value: b, label: b }))
|
|
19640
|
+
});
|
|
19641
|
+
if (p2.isCancel(result)) {
|
|
19642
|
+
p2.cancel("Sync cancelled.");
|
|
19643
|
+
return null;
|
|
19644
|
+
}
|
|
19645
|
+
return keyWorkspace(result, options.projectDir);
|
|
19646
|
+
}
|
|
19647
|
+
const existing = await resolveSyncWorkspace(branchName, options.projectDir, branches, byRef);
|
|
19648
|
+
if (existing)
|
|
19649
|
+
return existing;
|
|
19650
|
+
if (options.autoCreate)
|
|
19651
|
+
return createSyncWorkspace(branchName, options.projectDir);
|
|
19652
|
+
const available = branches.length > 0 ? ` Available branches: ${branches.join(", ")}` : "";
|
|
19653
|
+
throw new SyncError(`Branch workspace "${branchName}" not found.${available}`);
|
|
19654
|
+
}
|
|
19655
|
+
async function syncPush(options) {
|
|
19656
|
+
const { projectDir } = options;
|
|
19657
|
+
const contentRoot = options.contentRoot || "content";
|
|
19658
|
+
const srcContentDir = path47.join(projectDir, contentRoot);
|
|
19659
|
+
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
19660
|
+
if (!await filePathExists(srcContentDir)) {
|
|
19661
|
+
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
19662
|
+
}
|
|
19663
|
+
const ws = await selectBranch({ ...options, projectDir, autoCreate: true });
|
|
19664
|
+
if (!ws)
|
|
19665
|
+
return { fileCount: 0 };
|
|
19666
|
+
const { branchName, branchPath } = ws;
|
|
19667
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19668
|
+
const status = await wsGit.status();
|
|
19669
|
+
if (status.conflicted.length > 0) {
|
|
19670
|
+
p2.log.error("Branch workspace has unresolved merge conflicts.");
|
|
19671
|
+
if (!options.force) {
|
|
19672
|
+
const shouldAbort = await p2.confirm({
|
|
19673
|
+
message: "Abort the merge and restore workspace to pre-merge state?",
|
|
19674
|
+
initialValue: false
|
|
19675
|
+
});
|
|
19676
|
+
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
19677
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
19678
|
+
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
19679
|
+
}
|
|
19680
|
+
}
|
|
19681
|
+
}
|
|
19682
|
+
return { fileCount: 0 };
|
|
19683
|
+
}
|
|
19684
|
+
const editorChanges = status.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19685
|
+
if (editorChanges.length > 0 && !options.force) {
|
|
19686
|
+
p2.log.warn(`Branch workspace has ${editorChanges.length} uncommitted change(s) that will be committed to history then overwritten:`);
|
|
19687
|
+
for (const file of editorChanges.slice(0, 10)) {
|
|
19688
|
+
p2.log.warn(` ${file.path}`);
|
|
19689
|
+
}
|
|
19690
|
+
if (editorChanges.length > 10) {
|
|
19691
|
+
p2.log.warn(` ... and ${editorChanges.length - 10} more`);
|
|
19692
|
+
}
|
|
19693
|
+
const confirm5 = await p2.confirm({
|
|
19694
|
+
message: "Continue? Editor changes will be preserved in git history.",
|
|
19695
|
+
initialValue: false
|
|
19696
|
+
});
|
|
19697
|
+
if (p2.isCancel(confirm5) || !confirm5) {
|
|
19698
|
+
p2.cancel("Push cancelled.");
|
|
19699
|
+
return { fileCount: 0 };
|
|
19700
|
+
}
|
|
19701
|
+
}
|
|
19702
|
+
p2.log.step(`Pushing content to branch workspace: ${branchName}`);
|
|
19703
|
+
let fileCount;
|
|
19153
19704
|
try {
|
|
19154
|
-
|
|
19705
|
+
const result = await pushContentToWorkspace({
|
|
19706
|
+
srcContentDir,
|
|
19707
|
+
branchPath,
|
|
19708
|
+
contentRoot,
|
|
19709
|
+
baseTag: SYNC_BASE_TAG,
|
|
19710
|
+
saveEditorStateMessage: "sync: save editor state before push"
|
|
19711
|
+
});
|
|
19712
|
+
fileCount = result.fileCount;
|
|
19713
|
+
if (result.savedEditorState)
|
|
19714
|
+
p2.log.info("Committed editor changes to history before push");
|
|
19155
19715
|
} catch (err2) {
|
|
19156
|
-
throw
|
|
19716
|
+
throw lockBusyAsSyncError(err2, branchName);
|
|
19157
19717
|
}
|
|
19158
|
-
|
|
19159
|
-
|
|
19160
|
-
|
|
19718
|
+
if (fileCount === 0) {
|
|
19719
|
+
p2.log.info("Content is already up to date \u2014 nothing to push");
|
|
19720
|
+
} else {
|
|
19721
|
+
p2.log.success(`Pushed ${fileCount} file change(s) to branch "${branchName}"`);
|
|
19161
19722
|
}
|
|
19162
|
-
|
|
19163
|
-
return file;
|
|
19723
|
+
return { fileCount };
|
|
19164
19724
|
}
|
|
19165
|
-
|
|
19166
|
-
|
|
19167
|
-
|
|
19168
|
-
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19173
|
-
|
|
19174
|
-
|
|
19175
|
-
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19179
|
-
description: z18.string().optional(),
|
|
19180
|
-
status: enumOf2({
|
|
19181
|
-
editing: "editing",
|
|
19182
|
-
submitted: "submitted",
|
|
19183
|
-
approved: "approved",
|
|
19184
|
-
archived: "archived"
|
|
19185
|
-
}),
|
|
19186
|
-
access: z18.object({
|
|
19187
|
-
allowedUsers: z18.array(z18.string()).optional(),
|
|
19188
|
-
allowedGroups: z18.array(z18.string()).optional(),
|
|
19189
|
-
managerOrAdminAllowed: z18.boolean().optional()
|
|
19190
|
-
}).passthrough(),
|
|
19191
|
-
createdBy: z18.string().default("unknown"),
|
|
19192
|
-
createdAt: z18.string().default(UNKNOWN_TIME2),
|
|
19193
|
-
updatedAt: z18.string().default(UNKNOWN_TIME2),
|
|
19194
|
-
baseBranch: z18.string().optional(),
|
|
19195
|
-
pullRequestUrl: z18.string().optional(),
|
|
19196
|
-
pullRequestNumber: z18.number().int().optional(),
|
|
19197
|
-
submittedAt: z18.string().optional(),
|
|
19198
|
-
pushedToGitHubAt: z18.string().optional(),
|
|
19199
|
-
editorUrl: z18.string().optional(),
|
|
19200
|
-
syncStatus: enumOf2({
|
|
19201
|
-
synced: "synced",
|
|
19202
|
-
"pending-sync": "pending-sync",
|
|
19203
|
-
"sync-failed": "sync-failed"
|
|
19204
|
-
}).optional(),
|
|
19205
|
-
conflictStatus: enumOf2({
|
|
19206
|
-
clean: "clean",
|
|
19207
|
-
"conflicts-detected": "conflicts-detected"
|
|
19208
|
-
}).optional(),
|
|
19209
|
-
conflictFiles: z18.array(z18.custom((v) => typeof v === "string")).optional(),
|
|
19210
|
-
pullRequestState: enumOf2({
|
|
19211
|
-
open: "open",
|
|
19212
|
-
closed: "closed",
|
|
19213
|
-
merged: "merged"
|
|
19214
|
-
}).optional(),
|
|
19215
|
-
mergedAt: z18.string().optional(),
|
|
19216
|
-
rebaseFailure: z18.object({ message: z18.string(), firstAt: z18.string(), lastAt: z18.string() }).optional(),
|
|
19217
|
-
historyRewrittenFrom: z18.string().optional(),
|
|
19218
|
-
lastGitHubTip: z18.string().optional(),
|
|
19219
|
-
keptOverGitHub: z18.array(z18.string()).optional(),
|
|
19220
|
-
rewrittenOnGitHub: z18.string().optional(),
|
|
19221
|
-
submitUnfinished: z18.boolean().optional(),
|
|
19222
|
-
submitNotQueued: z18.boolean().optional(),
|
|
19223
|
-
unpublishedAtMerge: z18.boolean().optional(),
|
|
19224
|
-
syncFailureReason: z18.string().optional(),
|
|
19225
|
-
editors: z18.array(z18.string()).optional(),
|
|
19226
|
-
uncommittedEditors: z18.array(z18.string()).optional(),
|
|
19227
|
-
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
19228
|
-
pullRequestHold: z18.discriminatedUnion("kind", [
|
|
19229
|
-
z18.object({ kind: z18.literal("conflicts") }),
|
|
19230
|
-
z18.object({ kind: z18.literal("retargeted"), base: z18.string() })
|
|
19231
|
-
]).optional().catch(void 0),
|
|
19232
|
-
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
19233
|
-
review: z18.object({
|
|
19234
|
-
state: z18.enum(["approved", "changes-requested"]),
|
|
19235
|
-
by: z18.object({ userId: z18.string(), name: z18.string().optional() }),
|
|
19236
|
-
at: z18.string()
|
|
19237
|
-
}).optional().catch(void 0)
|
|
19238
|
-
};
|
|
19239
|
-
branchMetadataSchema2 = z18.object(branchMetadataShape2).passthrough();
|
|
19240
|
-
branchMetadataFileSchema2 = z18.object({
|
|
19241
|
-
schemaVersion: z18.number().int().default(1),
|
|
19242
|
-
version: z18.number().int().nonnegative().default(0),
|
|
19243
|
-
writeId: z18.string().optional(),
|
|
19244
|
-
branch: branchMetadataSchema2
|
|
19245
|
-
}).passthrough();
|
|
19725
|
+
async function syncPull(options) {
|
|
19726
|
+
const ws = await selectBranch({ ...options, projectDir: options.projectDir });
|
|
19727
|
+
if (!ws)
|
|
19728
|
+
return { fileCount: 0 };
|
|
19729
|
+
return pullFromWorkspace(options, ws);
|
|
19730
|
+
}
|
|
19731
|
+
async function pullFromWorkspace(options, ws) {
|
|
19732
|
+
const { projectDir } = options;
|
|
19733
|
+
const contentRoot = options.contentRoot || "content";
|
|
19734
|
+
const { branchName, branchPath } = ws;
|
|
19735
|
+
const branchContentDir = path47.join(branchPath, contentRoot);
|
|
19736
|
+
assertWithinDir(branchContentDir, branchPath, "--content-root");
|
|
19737
|
+
if (!await filePathExists(branchContentDir)) {
|
|
19738
|
+
throw new SyncError(`Content directory not found in branch workspace: ${path47.basename(branchPath)}/${contentRoot}`);
|
|
19246
19739
|
}
|
|
19247
|
-
});
|
|
19248
|
-
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
|
|
19262
|
-
|
|
19263
|
-
REGISTRY_FILE2 = "branches.json";
|
|
19264
|
-
REGISTRY_STALE_FILE2 = "branches.stale.json";
|
|
19265
|
-
REGISTRY_TEMP_FILE2 = "branches.tmp.json";
|
|
19266
|
-
REGISTRY_VERSION2 = 2;
|
|
19267
|
-
RESOURCE4 = "branch-registry";
|
|
19268
|
-
GET_MISS_REFRESH_MIN_INTERVAL_MS2 = 5e3;
|
|
19269
|
-
snapshotSchema2 = z19.object({
|
|
19270
|
-
version: z19.literal(REGISTRY_VERSION2),
|
|
19271
|
-
branches: z19.array(z19.object({ branch: branchMetadataSchema2, branchRoot: z19.string(), baseRoot: z19.string() })),
|
|
19272
|
-
generation: z19.string().nullable()
|
|
19273
|
-
});
|
|
19274
|
-
reportedUnreadable2 = /* @__PURE__ */ new Set();
|
|
19275
|
-
BranchRegistry2 = class {
|
|
19276
|
-
constructor(root) {
|
|
19277
|
-
this.regenInFlight = null;
|
|
19278
|
-
this.lastForcedRefreshMs = 0;
|
|
19279
|
-
this.root = path45.resolve(root);
|
|
19280
|
-
this.registryPath = path45.join(this.root, REGISTRY_FILE2);
|
|
19281
|
-
this.stalePath = path45.join(this.root, REGISTRY_STALE_FILE2);
|
|
19282
|
-
this.tempPath = path45.join(this.root, REGISTRY_TEMP_FILE2);
|
|
19740
|
+
p2.log.step(`Pulling content from branch: ${branchName}`);
|
|
19741
|
+
const destContentDir = path47.join(projectDir, contentRoot);
|
|
19742
|
+
assertWithinDir(destContentDir, projectDir, "--content-root");
|
|
19743
|
+
const sourceGit = simpleGit7({ baseDir: projectDir });
|
|
19744
|
+
const status = await sourceGit.status();
|
|
19745
|
+
const uncommittedContent = status.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
19746
|
+
const untrackedLosses = [];
|
|
19747
|
+
if (await filePathExists(destContentDir)) {
|
|
19748
|
+
const localFiles = await listFilesRecursive(destContentDir);
|
|
19749
|
+
const branchFiles = new Set(await listFilesRecursive(branchContentDir));
|
|
19750
|
+
for (const file of localFiles) {
|
|
19751
|
+
if (!branchFiles.has(file)) {
|
|
19752
|
+
const relativePath = contentRoot + "/" + file;
|
|
19753
|
+
if (!uncommittedContent.some((f) => f.path === relativePath)) {
|
|
19754
|
+
untrackedLosses.push(relativePath);
|
|
19755
|
+
}
|
|
19283
19756
|
}
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19291
|
-
|
|
19292
|
-
*/
|
|
19293
|
-
async list() {
|
|
19294
|
-
let raw;
|
|
19295
|
-
try {
|
|
19296
|
-
raw = await fs37.readFile(this.registryPath, "utf8");
|
|
19297
|
-
} catch (err2) {
|
|
19298
|
-
if (isNotFoundError(err2)) {
|
|
19299
|
-
return await this.regenerate();
|
|
19300
|
-
}
|
|
19301
|
-
throw err2;
|
|
19302
|
-
}
|
|
19303
|
-
const parsed = this.parseSnapshot(raw);
|
|
19304
|
-
if (!parsed)
|
|
19305
|
-
return await this.regenerate();
|
|
19306
|
-
const read = await readResourceGeneration2(this.root, RESOURCE4);
|
|
19307
|
-
if (isGenerationCurrent2(parsed.generation, read)) {
|
|
19308
|
-
return parsed.branches;
|
|
19309
|
-
}
|
|
19310
|
-
return await this.regenerate();
|
|
19311
|
-
}
|
|
19312
|
-
/** The snapshot in `raw`, or null when it must be rebuilt. */
|
|
19313
|
-
parseSnapshot(raw) {
|
|
19314
|
-
let json;
|
|
19315
|
-
try {
|
|
19316
|
-
json = JSON.parse(raw);
|
|
19317
|
-
} catch (err2) {
|
|
19318
|
-
this.reportUnreadable(getErrorMessage(err2));
|
|
19319
|
-
return null;
|
|
19320
|
-
}
|
|
19321
|
-
if (typeof json === "object" && json !== null && "version" in json && typeof json.version === "number" && json.version !== REGISTRY_VERSION2) {
|
|
19322
|
-
return null;
|
|
19323
|
-
}
|
|
19324
|
-
const result = snapshotSchema2.safeParse(json);
|
|
19325
|
-
if (!result.success) {
|
|
19326
|
-
this.reportUnreadable("it does not have the registry snapshot shape");
|
|
19327
|
-
return null;
|
|
19328
|
-
}
|
|
19329
|
-
reportedUnreadable2.delete(this.registryPath);
|
|
19330
|
-
const snapshot = result.data;
|
|
19331
|
-
return snapshot;
|
|
19332
|
-
}
|
|
19333
|
-
reportUnreadable(reason) {
|
|
19334
|
-
if (reportedUnreadable2.has(this.registryPath))
|
|
19335
|
-
return;
|
|
19336
|
-
reportedUnreadable2.add(this.registryPath);
|
|
19337
|
-
canopyLogWarn3(`CanopyCMS: Branch registry snapshot ${this.registryPath} is unreadable (${reason}); regenerating it from the branch directories`);
|
|
19338
|
-
}
|
|
19339
|
-
/**
|
|
19340
|
-
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
19341
|
-
*
|
|
19342
|
-
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
19343
|
-
* the snapshot may predate a branch that now exists, so force one fresh
|
|
19344
|
-
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
19345
|
-
* genuinely-absent branch costs at most one extra scan per window.
|
|
19346
|
-
*/
|
|
19347
|
-
async get(name) {
|
|
19348
|
-
return this.find((b) => b.branch.name === name);
|
|
19349
|
-
}
|
|
19350
|
-
/**
|
|
19351
|
-
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
19352
|
-
* whatever its name, with {@link get}'s backstop.
|
|
19353
|
-
*/
|
|
19354
|
-
async getByKey(key) {
|
|
19355
|
-
return this.find((b) => path45.basename(b.branchRoot) === key);
|
|
19757
|
+
}
|
|
19758
|
+
}
|
|
19759
|
+
const totalWarnings = uncommittedContent.length + untrackedLosses.length;
|
|
19760
|
+
if (totalWarnings > 0 && !options.force) {
|
|
19761
|
+
if (uncommittedContent.length > 0) {
|
|
19762
|
+
p2.log.warn(`You have ${uncommittedContent.length} uncommitted change(s) in ${contentRoot}/:`);
|
|
19763
|
+
for (const file of uncommittedContent.slice(0, 10)) {
|
|
19764
|
+
p2.log.warn(` ${file.path}`);
|
|
19356
19765
|
}
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
if (found)
|
|
19360
|
-
return found;
|
|
19361
|
-
if (!this.shouldForceRefresh())
|
|
19362
|
-
return void 0;
|
|
19363
|
-
const refreshed = await this.regenerate();
|
|
19364
|
-
return refreshed.find(matches);
|
|
19766
|
+
if (uncommittedContent.length > 10) {
|
|
19767
|
+
p2.log.warn(` ... and ${uncommittedContent.length - 10} more`);
|
|
19365
19768
|
}
|
|
19366
|
-
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
|
|
19371
|
-
return true;
|
|
19769
|
+
}
|
|
19770
|
+
if (untrackedLosses.length > 0) {
|
|
19771
|
+
p2.log.warn(`${untrackedLosses.length} file(s) in ${contentRoot}/ not present in branch "${branchName}" will be deleted:`);
|
|
19772
|
+
for (const file of untrackedLosses.slice(0, 10)) {
|
|
19773
|
+
p2.log.warn(` ${file}`);
|
|
19372
19774
|
}
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
19376
|
-
* swallowing that failure leaves the registry stale indefinitely with no
|
|
19377
|
-
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
19378
|
-
* bump alone already restored correctness for every future reader.
|
|
19379
|
-
*/
|
|
19380
|
-
async invalidate() {
|
|
19381
|
-
await bumpResourceGeneration2(this.root, RESOURCE4, { mustSucceed: true });
|
|
19382
|
-
try {
|
|
19383
|
-
if (this.regenInFlight)
|
|
19384
|
-
await this.regenInFlight.catch(() => {
|
|
19385
|
-
});
|
|
19386
|
-
await this.regenerate();
|
|
19387
|
-
} catch (err2) {
|
|
19388
|
-
log16.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
19389
|
-
error: getErrorMessage(err2)
|
|
19390
|
-
});
|
|
19391
|
-
}
|
|
19775
|
+
if (untrackedLosses.length > 10) {
|
|
19776
|
+
p2.log.warn(` ... and ${untrackedLosses.length - 10} more`);
|
|
19392
19777
|
}
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
|
|
19397
|
-
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19778
|
+
}
|
|
19779
|
+
const confirm5 = await p2.confirm({
|
|
19780
|
+
message: "Overwrite content directory? Files listed above will be lost.",
|
|
19781
|
+
initialValue: false
|
|
19782
|
+
});
|
|
19783
|
+
if (p2.isCancel(confirm5) || !confirm5) {
|
|
19784
|
+
p2.cancel("Pull cancelled. Commit or stash your changes first.");
|
|
19785
|
+
return { fileCount: 0 };
|
|
19786
|
+
}
|
|
19787
|
+
}
|
|
19788
|
+
const tmpDestDir = `${destContentDir}.sync-tmp-${Date.now()}`;
|
|
19789
|
+
try {
|
|
19790
|
+
await copyDir(branchContentDir, tmpDestDir);
|
|
19791
|
+
await safeReplaceDir(destContentDir, tmpDestDir);
|
|
19792
|
+
} catch (err2) {
|
|
19793
|
+
await fs38.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
|
|
19794
|
+
});
|
|
19795
|
+
throw err2;
|
|
19796
|
+
}
|
|
19797
|
+
const postStatus = await sourceGit.status();
|
|
19798
|
+
const contentChanges = postStatus.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
19799
|
+
if (contentChanges.length === 0) {
|
|
19800
|
+
p2.log.info("Content is already up to date \u2014 nothing to pull");
|
|
19801
|
+
} else {
|
|
19802
|
+
p2.log.success(`Pulled ${contentChanges.length} changed file(s) from branch "${branchName}"`);
|
|
19803
|
+
for (const file of contentChanges.slice(0, 20)) {
|
|
19804
|
+
const indicator = file.working_dir === "?" ? "A" : file.working_dir || file.index;
|
|
19805
|
+
p2.log.info(` ${indicator} ${file.path}`);
|
|
19806
|
+
}
|
|
19807
|
+
if (contentChanges.length > 20) {
|
|
19808
|
+
p2.log.info(` ... and ${contentChanges.length - 20} more`);
|
|
19809
|
+
}
|
|
19810
|
+
p2.log.info("\nReview the changes, then git add and commit when ready.");
|
|
19811
|
+
}
|
|
19812
|
+
return { fileCount: contentChanges.length };
|
|
19813
|
+
}
|
|
19814
|
+
async function syncBoth(options) {
|
|
19815
|
+
const { projectDir } = options;
|
|
19816
|
+
const contentRoot = options.contentRoot || "content";
|
|
19817
|
+
const srcContentDir = path47.join(projectDir, contentRoot);
|
|
19818
|
+
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
19819
|
+
if (!await filePathExists(srcContentDir)) {
|
|
19820
|
+
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
19821
|
+
}
|
|
19822
|
+
const ws = await selectBranch({ ...options, projectDir, autoCreate: true });
|
|
19823
|
+
if (!ws)
|
|
19824
|
+
return { pushed: 0, pulled: 0 };
|
|
19825
|
+
const { branchName, branchPath } = ws;
|
|
19826
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19827
|
+
const status = await wsGit.status();
|
|
19828
|
+
if (status.conflicted.length > 0) {
|
|
19829
|
+
p2.log.error("Branch workspace has unresolved merge conflicts.");
|
|
19830
|
+
if (!options.force) {
|
|
19831
|
+
const shouldAbort = await p2.confirm({
|
|
19832
|
+
message: "Abort the merge and restore workspace to pre-merge state?",
|
|
19833
|
+
initialValue: false
|
|
19834
|
+
});
|
|
19835
|
+
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
19836
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
19837
|
+
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
19408
19838
|
}
|
|
19409
19839
|
}
|
|
19410
|
-
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19840
|
+
}
|
|
19841
|
+
return { pushed: 0, pulled: 0 };
|
|
19842
|
+
}
|
|
19843
|
+
return withWorkspaceLock(branchPath, branchName, () => mergeIntoWorkspace(options, { branchName, branchPath, srcContentDir, contentRoot }));
|
|
19844
|
+
}
|
|
19845
|
+
async function mergeIntoWorkspace(options, ws) {
|
|
19846
|
+
const { branchPath, srcContentDir, contentRoot } = ws;
|
|
19847
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19848
|
+
const status = await wsGit.status();
|
|
19849
|
+
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
19850
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19851
|
+
await wsGit.commit("sync: save editor state before merge");
|
|
19852
|
+
p2.log.info("Committed editor changes before merge");
|
|
19853
|
+
}
|
|
19854
|
+
let baseRef;
|
|
19855
|
+
try {
|
|
19856
|
+
await wsGit.raw(["rev-parse", SYNC_BASE_TAG]);
|
|
19857
|
+
baseRef = SYNC_BASE_TAG;
|
|
19858
|
+
} catch {
|
|
19859
|
+
baseRef = "HEAD";
|
|
19860
|
+
}
|
|
19861
|
+
const currentBranch = (await wsGit.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
19862
|
+
try {
|
|
19863
|
+
const incomingBranch = `sync-incoming-${Date.now()}`;
|
|
19864
|
+
await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
|
|
19865
|
+
const wsContentDir = path47.join(branchPath, contentRoot);
|
|
19866
|
+
assertWithinDir(wsContentDir, branchPath, "--content-root");
|
|
19867
|
+
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
19868
|
+
try {
|
|
19869
|
+
await copyDir(srcContentDir, tmpDir);
|
|
19870
|
+
await safeReplaceDir(wsContentDir, tmpDir);
|
|
19871
|
+
} catch (err2) {
|
|
19872
|
+
await fs38.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
19873
|
+
});
|
|
19874
|
+
await wsGit.checkout(currentBranch);
|
|
19875
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19876
|
+
});
|
|
19877
|
+
throw err2;
|
|
19878
|
+
}
|
|
19879
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19880
|
+
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19881
|
+
if (incomingFiles.length === 0) {
|
|
19882
|
+
await wsGit.checkout(currentBranch);
|
|
19883
|
+
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
19884
|
+
p2.log.info("No working-tree changes to merge \u2014 pulling editor changes only");
|
|
19885
|
+
const pullResult2 = await pullFromWorkspace({ ...options, force: true }, ws);
|
|
19886
|
+
return { pushed: 0, pulled: pullResult2.fileCount };
|
|
19887
|
+
}
|
|
19888
|
+
await wsGit.commit("sync: incoming working-tree changes");
|
|
19889
|
+
await wsGit.checkout(currentBranch);
|
|
19890
|
+
p2.log.step("Merging working-tree changes with editor changes...");
|
|
19891
|
+
try {
|
|
19892
|
+
await wsGit.merge([incomingBranch, "--no-edit"]);
|
|
19893
|
+
} catch (mergeError) {
|
|
19894
|
+
let mergeStatus;
|
|
19895
|
+
try {
|
|
19896
|
+
mergeStatus = await wsGit.status();
|
|
19897
|
+
} catch {
|
|
19898
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19414
19899
|
});
|
|
19415
|
-
|
|
19416
|
-
return branches;
|
|
19417
|
-
}
|
|
19418
|
-
const snapshot = {
|
|
19419
|
-
version: REGISTRY_VERSION2,
|
|
19420
|
-
branches,
|
|
19421
|
-
generation: read.token
|
|
19422
|
-
};
|
|
19423
|
-
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
19424
|
-
await fs37.mkdir(this.root, { recursive: true });
|
|
19425
|
-
await fs37.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
19426
|
-
try {
|
|
19427
|
-
await fs37.rename(uniqueTempPath, this.registryPath);
|
|
19428
|
-
} catch (err2) {
|
|
19429
|
-
await fs37.unlink(uniqueTempPath).catch(() => {
|
|
19430
|
-
});
|
|
19431
|
-
throw err2;
|
|
19432
|
-
}
|
|
19433
|
-
return branches;
|
|
19900
|
+
throw mergeError;
|
|
19434
19901
|
}
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
|
|
19439
|
-
*/
|
|
19440
|
-
async scanBranchDirectories() {
|
|
19441
|
-
const branches = [];
|
|
19442
|
-
try {
|
|
19443
|
-
const entries = await fs37.readdir(this.root, { withFileTypes: true });
|
|
19444
|
-
for (const entry of entries) {
|
|
19445
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
19446
|
-
continue;
|
|
19447
|
-
}
|
|
19448
|
-
const branchRoot = path45.join(this.root, entry.name);
|
|
19449
|
-
let meta;
|
|
19450
|
-
try {
|
|
19451
|
-
meta = await readBranchMetadataFile2(branchRoot);
|
|
19452
|
-
} catch (err2) {
|
|
19453
|
-
canopyLogWarn3(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err2)}`);
|
|
19454
|
-
continue;
|
|
19455
|
-
}
|
|
19456
|
-
if (meta) {
|
|
19457
|
-
branches.push({
|
|
19458
|
-
branch: meta.branch,
|
|
19459
|
-
branchRoot,
|
|
19460
|
-
baseRoot: this.root
|
|
19461
|
-
});
|
|
19462
|
-
}
|
|
19463
|
-
}
|
|
19464
|
-
} catch (err2) {
|
|
19465
|
-
if (isNotFoundError(err2)) {
|
|
19466
|
-
return [];
|
|
19467
|
-
}
|
|
19468
|
-
throw err2;
|
|
19902
|
+
if (mergeStatus.conflicted.length > 0) {
|
|
19903
|
+
p2.log.error("Merge conflicts detected in the following files:");
|
|
19904
|
+
for (const file of mergeStatus.conflicted) {
|
|
19905
|
+
p2.log.error(` ${file}`);
|
|
19469
19906
|
}
|
|
19470
|
-
|
|
19907
|
+
p2.log.info("The branch workspace is now in a merge state.");
|
|
19908
|
+
const branchFlag = options.branch ? ` --branch ${options.branch}` : "";
|
|
19909
|
+
p2.log.info(`Resolve the conflicts in the workspace, then run: canopycms sync pull${branchFlag}`);
|
|
19910
|
+
p2.log.info(`Or abort the merge with: canopycms sync abort${branchFlag}`);
|
|
19911
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19912
|
+
});
|
|
19913
|
+
return { pushed: 0, pulled: 0 };
|
|
19914
|
+
}
|
|
19915
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19916
|
+
});
|
|
19917
|
+
throw mergeError;
|
|
19918
|
+
}
|
|
19919
|
+
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
19920
|
+
await wsGit.tag(["-f", SYNC_BASE_TAG]);
|
|
19921
|
+
p2.log.success("Merged working-tree changes with editor changes");
|
|
19922
|
+
const pullResult = await pullFromWorkspace({ ...options, force: true }, ws);
|
|
19923
|
+
return { pushed: incomingFiles.length, pulled: pullResult.fileCount };
|
|
19924
|
+
} finally {
|
|
19925
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
19926
|
+
}
|
|
19927
|
+
}
|
|
19928
|
+
async function syncAbort(options) {
|
|
19929
|
+
const ws = await selectBranch({ ...options, projectDir: options.projectDir });
|
|
19930
|
+
if (!ws)
|
|
19931
|
+
return;
|
|
19932
|
+
const { branchName, branchPath } = ws;
|
|
19933
|
+
if (!await abortWorkspaceMerge(branchPath, branchName)) {
|
|
19934
|
+
p2.log.info(`Branch workspace "${branchName}" is not in a merge state \u2014 nothing to abort.`);
|
|
19935
|
+
return;
|
|
19936
|
+
}
|
|
19937
|
+
p2.log.success(`Merge aborted in branch workspace "${branchName}". Workspace restored to pre-merge state.`);
|
|
19938
|
+
}
|
|
19939
|
+
async function sync(options) {
|
|
19940
|
+
p2.intro("CanopyCMS sync");
|
|
19941
|
+
let pushed = 0;
|
|
19942
|
+
let pulled = 0;
|
|
19943
|
+
if (options.direction === "abort") {
|
|
19944
|
+
await syncAbort(options);
|
|
19945
|
+
} else if (options.direction === "both") {
|
|
19946
|
+
const result = await syncBoth(options);
|
|
19947
|
+
pushed = result.pushed;
|
|
19948
|
+
pulled = result.pulled;
|
|
19949
|
+
} else if (options.direction === "push") {
|
|
19950
|
+
const result = await syncPush(options);
|
|
19951
|
+
pushed = result.fileCount;
|
|
19952
|
+
} else {
|
|
19953
|
+
const result = await syncPull(options);
|
|
19954
|
+
pulled = result.fileCount;
|
|
19955
|
+
}
|
|
19956
|
+
p2.outro("Done!");
|
|
19957
|
+
return { pushed, pulled };
|
|
19958
|
+
}
|
|
19959
|
+
var SyncError, branchConfigSchema;
|
|
19960
|
+
var init_sync = __esm({
|
|
19961
|
+
"dist/cli/sync.js"() {
|
|
19962
|
+
"use strict";
|
|
19963
|
+
init_branch_param();
|
|
19964
|
+
init_branch_metadata_file();
|
|
19965
|
+
init_branch();
|
|
19966
|
+
init_validation3();
|
|
19967
|
+
init_error();
|
|
19968
|
+
init_fs();
|
|
19969
|
+
init_git();
|
|
19970
|
+
init_sync_core();
|
|
19971
|
+
init_content_index_generation();
|
|
19972
|
+
init_content_write_lock();
|
|
19973
|
+
init_git_manager();
|
|
19974
|
+
init_operating_mode2();
|
|
19975
|
+
init_site_config();
|
|
19976
|
+
SyncError = class extends Error {
|
|
19977
|
+
constructor(message) {
|
|
19978
|
+
super(message);
|
|
19979
|
+
this.name = "SyncError";
|
|
19471
19980
|
}
|
|
19472
19981
|
};
|
|
19982
|
+
branchConfigSchema = z19.object({
|
|
19983
|
+
branchNamespace: z19.string().optional(),
|
|
19984
|
+
deploymentName: z19.string().optional(),
|
|
19985
|
+
defaultBaseBranch: z19.string().optional(),
|
|
19986
|
+
defaultActiveBranch: z19.string().optional()
|
|
19987
|
+
});
|
|
19473
19988
|
}
|
|
19474
19989
|
});
|
|
19475
19990
|
|
|
19476
|
-
// dist/
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
}
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
createdBy: "unknown",
|
|
19491
|
-
createdAt: now,
|
|
19492
|
-
updatedAt: now
|
|
19493
|
-
};
|
|
19494
|
-
return {
|
|
19495
|
-
// Envelope keys this code does not know survive, as unknown branch keys do below.
|
|
19496
|
-
...existing,
|
|
19497
|
-
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
19498
|
-
version: version ?? 0,
|
|
19499
|
-
branch: {
|
|
19500
|
-
...defaults,
|
|
19501
|
-
...existing?.branch,
|
|
19502
|
-
...incoming.branch,
|
|
19503
|
-
access: {
|
|
19504
|
-
...existing?.branch?.access,
|
|
19505
|
-
...incoming.branch?.access
|
|
19506
|
-
},
|
|
19507
|
-
// Immutable after creation
|
|
19508
|
-
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
19509
|
-
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
19510
|
-
// Fork point is recorded once at creation; later saves must not move it
|
|
19511
|
-
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
19512
|
-
// Always stamped fresh; the spreads above would otherwise let the creation-time value win
|
|
19513
|
-
// forever, freezing the timestamp the editor's Branches panel sorts and displays by
|
|
19514
|
-
updatedAt: now
|
|
19515
|
-
}
|
|
19516
|
-
};
|
|
19991
|
+
// dist/schema/index.js
|
|
19992
|
+
var init_schema = __esm({
|
|
19993
|
+
"dist/schema/index.js"() {
|
|
19994
|
+
"use strict";
|
|
19995
|
+
init_meta_loader();
|
|
19996
|
+
}
|
|
19997
|
+
});
|
|
19998
|
+
|
|
19999
|
+
// dist/schema/schema-store.js
|
|
20000
|
+
import path48 from "node:path";
|
|
20001
|
+
import { z as z20 } from "zod";
|
|
20002
|
+
async function withBranchSchemaLock(branchRoot, fn, contentWriteLockWaitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
20003
|
+
const schemaLockPath = path48.join(path48.resolve(branchRoot), ".canopy-meta", "schema");
|
|
20004
|
+
return withContentWriteLock2(branchRoot, () => withLock2(schemaLockPath, () => withOccFileLock2(schemaLockPath, fn)), contentWriteLockWaitMs);
|
|
19517
20005
|
}
|
|
19518
|
-
var
|
|
19519
|
-
var
|
|
19520
|
-
"dist/
|
|
20006
|
+
var MAX_NAME_LENGTH, MAX_LABEL_LENGTH, SAFE_NAME_PATTERN, SAFE_NAME_MESSAGE, entryTypeInputSchema, createCollectionInputSchema, updateCollectionInputSchema, updateEntryTypeInputSchema, log16;
|
|
20007
|
+
var init_schema_store = __esm({
|
|
20008
|
+
"dist/schema/schema-store.js"() {
|
|
19521
20009
|
"use strict";
|
|
19522
|
-
init_branch_registry();
|
|
19523
|
-
init_branch_metadata_file();
|
|
19524
|
-
init_error();
|
|
19525
20010
|
init_async_mutex();
|
|
20011
|
+
init_debug2();
|
|
20012
|
+
init_content_write_lock();
|
|
19526
20013
|
init_occ_json_write();
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
}
|
|
19533
|
-
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
}
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
|
|
19559
|
-
|
|
19560
|
-
return { meta, version: meta?.version ?? null };
|
|
19561
|
-
}
|
|
19562
|
-
/**
|
|
19563
|
-
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
19564
|
-
* default here since payload shaping is branch-metadata's concern.
|
|
19565
|
-
*
|
|
19566
|
-
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
19567
|
-
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
19568
|
-
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
19569
|
-
* save() boundary would make withOccRetry's predicate miss it.
|
|
19570
|
-
*
|
|
19571
|
-
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
19572
|
-
* unlike comments.json.
|
|
19573
|
-
*/
|
|
19574
|
-
async write(meta, expectedVersion) {
|
|
19575
|
-
const payload = {
|
|
19576
|
-
...meta,
|
|
19577
|
-
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION2
|
|
19578
|
-
};
|
|
19579
|
-
return writeOccJsonFile2(this.filePath, payload, {
|
|
19580
|
-
expectedVersion,
|
|
19581
|
-
settleMs: this.settleMs,
|
|
19582
|
-
trailingNewline: true
|
|
19583
|
-
});
|
|
19584
|
-
}
|
|
19585
|
-
/**
|
|
19586
|
-
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
19587
|
-
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
19588
|
-
*
|
|
19589
|
-
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
19590
|
-
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
19591
|
-
* removes the branch directory, while the save reaches here after it, and
|
|
19592
|
-
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
19593
|
-
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
19594
|
-
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
19595
|
-
* save fails fast instead of paying for a lock.
|
|
19596
|
-
*
|
|
19597
|
-
* A save that passes the check just before a delete still cannot write: the
|
|
19598
|
-
* delete renames the tree away under the same lockfile, and that lockfile
|
|
19599
|
-
* creates only its own directory (`withOccFileLock`), so the save fails there.
|
|
19600
|
-
*/
|
|
19601
|
-
async save(incoming) {
|
|
19602
|
-
const saved = await this.saveIf(incoming, () => true);
|
|
19603
|
-
if (saved === null)
|
|
19604
|
-
throw new Error("unreachable: an unconditional save was skipped");
|
|
19605
|
-
return saved;
|
|
19606
|
-
}
|
|
19607
|
-
/**
|
|
19608
|
-
* {@link save}, applied only when `onlyIf` accepts the metadata on disk; resolves null when it
|
|
19609
|
-
* does not, having written nothing. `onlyIf` runs inside the lock stack against the version
|
|
19610
|
-
* the write is checked against, so the decision and the write are one compare-and-set.
|
|
19611
|
-
*/
|
|
19612
|
-
async saveIf(incoming, onlyIf) {
|
|
19613
|
-
return this.update((existing) => onlyIf(existing) ? incoming : null);
|
|
19614
|
-
}
|
|
19615
|
-
/**
|
|
19616
|
-
* A save whose update `compute` derives from the metadata on disk, inside the lock stack, so
|
|
19617
|
-
* an update built from the existing value (adding to a list) cannot lose a concurrent one.
|
|
19618
|
-
* `compute` returning null writes nothing and resolves null. `keepUpdatedAt` leaves
|
|
19619
|
-
* `updatedAt` as it was, for bookkeeping writes that are not a change to the branch.
|
|
19620
|
-
*/
|
|
19621
|
-
async update(compute, options = {}) {
|
|
19622
|
-
try {
|
|
19623
|
-
await fs38.stat(this.branchRoot);
|
|
19624
|
-
} catch (err2) {
|
|
19625
|
-
if (isNotFoundError(err2)) {
|
|
19626
|
-
throw new BranchMetadataConflictError2("Branch no longer exists");
|
|
19627
|
-
}
|
|
19628
|
-
throw err2;
|
|
19629
|
-
}
|
|
19630
|
-
let saved;
|
|
19631
|
-
try {
|
|
19632
|
-
saved = await withLock2(this.filePath, () => withOccFileLock2(this.filePath, () => withOccRetry2(async () => {
|
|
19633
|
-
const { meta: existing, version } = await this.load();
|
|
19634
|
-
const incoming = compute(existing);
|
|
19635
|
-
if (incoming === null)
|
|
19636
|
-
return null;
|
|
19637
|
-
const merged = mergeBranchMetadata2(existing, version, incoming);
|
|
19638
|
-
if (options.keepUpdatedAt && existing)
|
|
19639
|
-
merged.branch.updatedAt = existing.branch.updatedAt;
|
|
19640
|
-
const written = await this.write(merged, version);
|
|
19641
|
-
merged.version = written.version;
|
|
19642
|
-
merged.writeId = written.writeId;
|
|
19643
|
-
return merged;
|
|
19644
|
-
})));
|
|
19645
|
-
} catch (err2) {
|
|
19646
|
-
if (err2 instanceof OccWriteConflictError2) {
|
|
19647
|
-
throw new BranchMetadataConflictError2();
|
|
19648
|
-
}
|
|
19649
|
-
throw err2;
|
|
19650
|
-
}
|
|
19651
|
-
if (saved !== null)
|
|
19652
|
-
await this.invalidateRegistry();
|
|
19653
|
-
return saved;
|
|
19654
|
-
}
|
|
19655
|
-
/**
|
|
19656
|
-
* Adds `userId` to `editors` and `uncommittedEditors`. A lock-free read skips the write when
|
|
19657
|
-
* both already hold it, so only a user's first save since the last submit commit pays for the lock.
|
|
19658
|
-
* Never creates branch.json.
|
|
19659
|
-
*/
|
|
19660
|
-
async recordEditor(userId) {
|
|
19661
|
-
const current = await readBranchMetadataFile2(this.branchRoot);
|
|
19662
|
-
if (!current || isRecordedEditor2(current.branch, userId))
|
|
19663
|
-
return;
|
|
19664
|
-
await this.update((existing) => existing && !isRecordedEditor2(existing.branch, userId) ? {
|
|
19665
|
-
branch: {
|
|
19666
|
-
editors: withMember2(existing.branch.editors, userId),
|
|
19667
|
-
uncommittedEditors: withMember2(existing.branch.uncommittedEditors, userId)
|
|
19668
|
-
}
|
|
19669
|
-
} : null, { keepUpdatedAt: true });
|
|
19670
|
-
}
|
|
19671
|
-
/**
|
|
19672
|
-
* Removes `userIds` from `uncommittedEditors` once a submit commit's trailers name them. Ids
|
|
19673
|
-
* recorded after the commit read them stay, so the next commit names them.
|
|
19674
|
-
*/
|
|
19675
|
-
async markEditorsCommitted(userIds) {
|
|
19676
|
-
const committed = new Set(userIds);
|
|
19677
|
-
if (committed.size === 0)
|
|
19678
|
-
return;
|
|
19679
|
-
await this.update((existing) => {
|
|
19680
|
-
const pending = existing?.branch.uncommittedEditors ?? [];
|
|
19681
|
-
const remaining = pending.filter((id) => !committed.has(id));
|
|
19682
|
-
if (remaining.length === pending.length)
|
|
19683
|
-
return null;
|
|
19684
|
-
return { branch: { uncommittedEditors: remaining.length > 0 ? remaining : void 0 } };
|
|
19685
|
-
}, { keepUpdatedAt: true });
|
|
19686
|
-
}
|
|
19687
|
-
/** Invalidate the registry cache so the next list() regenerates. */
|
|
19688
|
-
async invalidateRegistry() {
|
|
19689
|
-
const registry3 = new BranchRegistry2(this.baseRoot);
|
|
19690
|
-
await registry3.invalidate();
|
|
19691
|
-
}
|
|
19692
|
-
};
|
|
20014
|
+
MAX_NAME_LENGTH = 64;
|
|
20015
|
+
MAX_LABEL_LENGTH = 128;
|
|
20016
|
+
SAFE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
20017
|
+
SAFE_NAME_MESSAGE = "must start with a letter and contain only lowercase letters, numbers, and hyphens";
|
|
20018
|
+
entryTypeInputSchema = z20.object({
|
|
20019
|
+
name: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Entry type name ${SAFE_NAME_MESSAGE}`),
|
|
20020
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
20021
|
+
format: z20.enum(["md", "mdx", "json", "yaml"]),
|
|
20022
|
+
schema: z20.string().min(1),
|
|
20023
|
+
default: z20.boolean().optional(),
|
|
20024
|
+
maxItems: z20.number().int().positive().optional()
|
|
20025
|
+
});
|
|
20026
|
+
createCollectionInputSchema = z20.object({
|
|
20027
|
+
name: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`),
|
|
20028
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
20029
|
+
parentPath: z20.string().optional(),
|
|
20030
|
+
entries: z20.array(entryTypeInputSchema).min(1, "Collection must have at least one entry type")
|
|
20031
|
+
});
|
|
20032
|
+
updateCollectionInputSchema = z20.object({
|
|
20033
|
+
name: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`).optional(),
|
|
20034
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
20035
|
+
// Directory name (e.g., "posts" in "posts.{id}/")
|
|
20036
|
+
slug: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Slug ${SAFE_NAME_MESSAGE}`).optional(),
|
|
20037
|
+
order: z20.array(z20.string()).optional()
|
|
20038
|
+
});
|
|
20039
|
+
updateEntryTypeInputSchema = z20.object({
|
|
20040
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
20041
|
+
format: z20.enum(["md", "mdx", "json", "yaml"]).optional(),
|
|
20042
|
+
schema: z20.string().min(1).optional(),
|
|
20043
|
+
default: z20.boolean().optional(),
|
|
20044
|
+
maxItems: z20.number().int().positive().optional()
|
|
20045
|
+
});
|
|
20046
|
+
log16 = createDebugLogger3({ prefix: "SchemaOps" });
|
|
19693
20047
|
}
|
|
19694
20048
|
});
|
|
19695
20049
|
|
|
@@ -19701,7 +20055,7 @@ __export(migrate_exports, {
|
|
|
19701
20055
|
slugifyName: () => slugifyName
|
|
19702
20056
|
});
|
|
19703
20057
|
import fs39 from "node:fs/promises";
|
|
19704
|
-
import
|
|
20058
|
+
import path49 from "node:path";
|
|
19705
20059
|
import * as p3 from "@clack/prompts";
|
|
19706
20060
|
function slugifyName(name) {
|
|
19707
20061
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -19735,7 +20089,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
19735
20089
|
if (entry.name.startsWith("."))
|
|
19736
20090
|
continue;
|
|
19737
20091
|
if (entry.isDirectory()) {
|
|
19738
|
-
const subPath =
|
|
20092
|
+
const subPath = path49.join(dirPath, entry.name);
|
|
19739
20093
|
const sub = await planDirectory(subPath, false, ctx);
|
|
19740
20094
|
if (!sub.hasContent)
|
|
19741
20095
|
continue;
|
|
@@ -19743,10 +20097,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
19743
20097
|
ops.push(...sub.ops);
|
|
19744
20098
|
if (!extractIdFromFilename2(entry.name)) {
|
|
19745
20099
|
const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
|
|
19746
|
-
ops.push({ kind: "rename", from: subPath, to:
|
|
20100
|
+
ops.push({ kind: "rename", from: subPath, to: path49.join(dirPath, newName) });
|
|
19747
20101
|
}
|
|
19748
20102
|
} else if (entry.isFile()) {
|
|
19749
|
-
const ext =
|
|
20103
|
+
const ext = path49.extname(entry.name).slice(1);
|
|
19750
20104
|
const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
|
|
19751
20105
|
if (!matchesFormat)
|
|
19752
20106
|
continue;
|
|
@@ -19757,17 +20111,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
19757
20111
|
const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
|
|
19758
20112
|
ops.push({
|
|
19759
20113
|
kind: "rename",
|
|
19760
|
-
from:
|
|
19761
|
-
to:
|
|
20114
|
+
from: path49.join(dirPath, entry.name),
|
|
20115
|
+
to: path49.join(dirPath, newName)
|
|
19762
20116
|
});
|
|
19763
20117
|
}
|
|
19764
20118
|
}
|
|
19765
20119
|
}
|
|
19766
20120
|
const needsMeta = isRoot ? hasOwnFiles : hasContent;
|
|
19767
|
-
const metaPath =
|
|
20121
|
+
const metaPath = path49.join(dirPath, ".collection.json");
|
|
19768
20122
|
if (needsMeta && !await filePathExists(metaPath)) {
|
|
19769
20123
|
const entryTypeMeta = { name: ctx.entryType, format: ctx.format, schema: ctx.schema };
|
|
19770
|
-
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(
|
|
20124
|
+
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path49.basename(dirPath))), entries: [entryTypeMeta] };
|
|
19771
20125
|
ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
|
|
19772
20126
|
}
|
|
19773
20127
|
return { ops, hasContent };
|
|
@@ -19776,7 +20130,7 @@ async function migrate(options) {
|
|
|
19776
20130
|
const { projectDir } = options;
|
|
19777
20131
|
const contentRoot = options.contentRoot || "content";
|
|
19778
20132
|
p3.intro("CanopyCMS migrate");
|
|
19779
|
-
const contentDir =
|
|
20133
|
+
const contentDir = path49.join(projectDir, contentRoot);
|
|
19780
20134
|
if (!await filePathExists(contentDir)) {
|
|
19781
20135
|
throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
|
|
19782
20136
|
}
|
|
@@ -19911,7 +20265,7 @@ var init_migrate = __esm({
|
|
|
19911
20265
|
this.name = "MigrateError";
|
|
19912
20266
|
}
|
|
19913
20267
|
};
|
|
19914
|
-
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${
|
|
20268
|
+
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path49.relative(baseDir, op.from)} \u2192 ${path49.basename(op.to)}` : `create ${path49.relative(baseDir, op.filePath)}`;
|
|
19915
20269
|
}
|
|
19916
20270
|
});
|
|
19917
20271
|
|