canopycms 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -47
- package/dist/ai/resolve-branch.js +2 -1
- package/dist/api/admin-branch-health.d.ts +11 -0
- package/dist/api/admin-branch-health.js +36 -0
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.js +20 -3
- package/dist/api/branch-status.js +30 -19
- package/dist/api/branch.d.ts +8 -3
- package/dist/api/branch.js +32 -3
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/github-sync.d.ts +2 -0
- package/dist/api/github-sync.js +6 -1
- package/dist/api/pr-comment-sync.js +3 -1
- 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/branch-metadata-file.d.ts +66 -0
- package/dist/branch-metadata-file.js +14 -0
- package/dist/branch-metadata.d.ts +4 -2
- package/dist/branch-metadata.js +10 -2
- package/dist/cli/cli.js +1913 -1606
- package/dist/cli/configured-asset-store.js +2 -8
- package/dist/cli/generate-ai-content.js +17 -4
- package/dist/cli/init.js +2 -2
- 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/editor/BranchManager.d.ts +8 -4
- package/dist/editor/BranchManager.js +43 -18
- package/dist/editor/Editor.js +3 -2
- package/dist/editor/admin/SystemHealthPanel.js +65 -31
- package/dist/editor/admin/UnpublishedAtMergeAlert.d.ts +16 -0
- package/dist/editor/admin/UnpublishedAtMergeAlert.js +50 -0
- package/dist/editor/admin/useSystemHealth.d.ts +2 -0
- package/dist/editor/admin/useSystemHealth.js +21 -0
- package/dist/editor/branch-lock.d.ts +2 -0
- package/dist/editor/branch-lock.js +5 -3
- package/dist/editor/branch-status.d.ts +25 -1
- package/dist/editor/branch-status.js +75 -0
- package/dist/editor/components/EditorHeader.d.ts +6 -2
- package/dist/editor/components/EditorHeader.js +51 -30
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +2 -1
- package/dist/editor/components/TooltipDetail.d.ts +14 -0
- package/dist/editor/components/TooltipDetail.js +18 -0
- package/dist/editor/copy.d.ts +7 -0
- package/dist/editor/copy.js +9 -0
- package/dist/editor/hooks/useBranchManager.d.ts +8 -3
- package/dist/editor/hooks/useBranchManager.js +48 -5
- package/dist/pr-comments.d.ts +2 -0
- package/dist/pr-comments.js +20 -14
- package/dist/resource-generation.js +2 -3
- package/dist/services.d.ts +3 -2
- package/dist/submission-attribution.d.ts +1 -1
- package/dist/submission-attribution.js +20 -2
- package/dist/system-users.d.ts +2 -0
- package/dist/system-users.js +6 -1
- package/dist/task-queue/task-queue.d.ts +5 -1
- package/dist/task-queue/task-queue.js +4 -2
- package/dist/types.d.ts +61 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +68 -0
- package/dist/worker/branch-merge.js +228 -0
- package/dist/worker/canopy-state.d.ts +2 -0
- package/dist/worker/canopy-state.js +1 -1
- package/dist/worker/cms-worker.d.ts +1 -0
- package/dist/worker/cms-worker.js +5 -3
- package/dist/worker/git-sync.js +15 -8
- package/dist/worker/github-commits.d.ts +106 -0
- package/dist/worker/github-commits.js +485 -0
- package/dist/worker/github-gateway-http.js +3 -2
- package/dist/worker/github-gateway-wire.d.ts +22 -13
- package/dist/worker/github-gateway-wire.js +4 -1
- package/dist/worker/github-gateway.d.ts +16 -5
- package/dist/worker/github-gateway.js +22 -5
- package/dist/worker/github-mirror.d.ts +8 -0
- package/dist/worker/github-mirror.js +12 -1
- package/dist/worker/github-transfer.d.ts +3 -1
- package/dist/worker/github-transfer.js +3 -2
- package/dist/worker/history-rewrite.d.ts +13 -1
- package/dist/worker/history-rewrite.js +27 -3
- package/dist/worker/rebase.d.ts +13 -7
- package/dist/worker/rebase.js +351 -95
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +172 -34
- package/dist/worker/worker-context.d.ts +2 -0
- package/package.json +1 -1
package/dist/cli/cli.js
CHANGED
|
@@ -562,7 +562,7 @@ async function failTask(taskDir, taskId, error, logger = nullLogger) {
|
|
|
562
562
|
});
|
|
563
563
|
logger.debug("Failed task", { id: taskId, error });
|
|
564
564
|
}
|
|
565
|
-
async function retryTask(taskDir, taskId, error, logger = nullLogger) {
|
|
565
|
+
async function retryTask(taskDir, taskId, error, logger = nullLogger, options = {}) {
|
|
566
566
|
const processingPath = path6.join(taskDir, "processing", `${taskId}.json`);
|
|
567
567
|
const pendingDir = path6.join(taskDir, "pending");
|
|
568
568
|
const pendingPath = path6.join(pendingDir, `${taskId}.json`);
|
|
@@ -583,7 +583,7 @@ async function retryTask(taskDir, taskId, error, logger = nullLogger) {
|
|
|
583
583
|
throw err2;
|
|
584
584
|
}
|
|
585
585
|
const retryCount = (task.retryCount ?? 0) + 1;
|
|
586
|
-
const backoffMs = Math.min(5e3 * Math.pow(2, retryCount - 1), 6e4);
|
|
586
|
+
const backoffMs = options.delayMs ?? Math.min(5e3 * Math.pow(2, retryCount - 1), 6e4);
|
|
587
587
|
task.status = "pending";
|
|
588
588
|
task.retryCount = retryCount;
|
|
589
589
|
task.retryAfter = new Date(Date.now() + backoffMs).toISOString();
|
|
@@ -5357,9 +5357,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
5357
5357
|
await atomicWriteFile2(resourceGenerationPath(root, resource), token);
|
|
5358
5358
|
return token;
|
|
5359
5359
|
} catch (err2) {
|
|
5360
|
-
|
|
5361
|
-
error: getErrorMessage3(err2)
|
|
5362
|
-
});
|
|
5360
|
+
canopyLogWarn2(`CanopyCMS: failed to bump ${resource} generation for ${root}:`, getErrorMessage3(err2));
|
|
5363
5361
|
if (options?.mustSucceed)
|
|
5364
5362
|
throw err2;
|
|
5365
5363
|
return null;
|
|
@@ -7907,7 +7905,7 @@ async function resolveBranchRoot(config) {
|
|
|
7907
7905
|
config: resolved,
|
|
7908
7906
|
branchName: activeBranch,
|
|
7909
7907
|
mode: config.mode,
|
|
7910
|
-
createdBy:
|
|
7908
|
+
createdBy: AI_USER_ID,
|
|
7911
7909
|
remoteUrl: config.defaultRemoteUrl
|
|
7912
7910
|
});
|
|
7913
7911
|
return context.branchRoot;
|
|
@@ -8436,7 +8434,7 @@ async function generateAIContentCLI(options) {
|
|
|
8436
8434
|
console.log(` Output: ${result.outputDir}
|
|
8437
8435
|
`);
|
|
8438
8436
|
}
|
|
8439
|
-
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;
|
|
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, 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;
|
|
8440
8438
|
var init_generate_ai_content = __esm({
|
|
8441
8439
|
"dist/cli/generate-ai-content.js"() {
|
|
8442
8440
|
"use strict";
|
|
@@ -10746,6 +10744,7 @@ var init_generate_ai_content = __esm({
|
|
|
10746
10744
|
init_field_traversal();
|
|
10747
10745
|
init_atomic_write2();
|
|
10748
10746
|
init_error3();
|
|
10747
|
+
init_logger2();
|
|
10749
10748
|
log4 = createDebugLogger2({ prefix: "ResourceGeneration" });
|
|
10750
10749
|
META_DIR2 = ".canopy-meta";
|
|
10751
10750
|
init_logger2();
|
|
@@ -12394,9 +12393,20 @@ var init_generate_ai_content = __esm({
|
|
|
12394
12393
|
mergedAt: z72.string().optional(),
|
|
12395
12394
|
rebaseFailure: z72.object({ message: z72.string(), firstAt: z72.string(), lastAt: z72.string() }).optional(),
|
|
12396
12395
|
historyRewrittenFrom: z72.string().optional(),
|
|
12396
|
+
lastGitHubTip: z72.string().optional(),
|
|
12397
|
+
keptOverGitHub: z72.array(z72.string()).optional(),
|
|
12398
|
+
rewrittenOnGitHub: z72.string().optional(),
|
|
12399
|
+
submitUnfinished: z72.boolean().optional(),
|
|
12400
|
+
submitNotQueued: z72.boolean().optional(),
|
|
12401
|
+
unpublishedAtMerge: z72.boolean().optional(),
|
|
12397
12402
|
syncFailureReason: z72.string().optional(),
|
|
12398
12403
|
editors: z72.array(z72.string()).optional(),
|
|
12399
12404
|
uncommittedEditors: z72.array(z72.string()).optional(),
|
|
12405
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
12406
|
+
pullRequestHold: z72.discriminatedUnion("kind", [
|
|
12407
|
+
z72.object({ kind: z72.literal("conflicts") }),
|
|
12408
|
+
z72.object({ kind: z72.literal("retargeted"), base: z72.string() })
|
|
12409
|
+
]).optional().catch(void 0),
|
|
12400
12410
|
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
12401
12411
|
review: z72.object({
|
|
12402
12412
|
state: z72.enum(["approved", "changes-requested"]),
|
|
@@ -14583,6 +14593,7 @@ var init_generate_ai_content = __esm({
|
|
|
14583
14593
|
return { kind: "exists", context: { branch: meta.branch, branchRoot, baseRoot } };
|
|
14584
14594
|
}
|
|
14585
14595
|
};
|
|
14596
|
+
AI_USER_ID = "canopycms-ai";
|
|
14586
14597
|
init_error3();
|
|
14587
14598
|
init_item_keys();
|
|
14588
14599
|
log10 = createDebugLogger2({ prefix: "ContentListing" });
|
|
@@ -17035,6 +17046,10 @@ function sanitizeBranchName3(branchName) {
|
|
|
17035
17046
|
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
17036
17047
|
return trimmedDots || "branch";
|
|
17037
17048
|
}
|
|
17049
|
+
function isSettingsBranchName2(branchName, settingsBranchName) {
|
|
17050
|
+
const sanitized = sanitizeBranchName3(branchName);
|
|
17051
|
+
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX3) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName3(settingsBranchName);
|
|
17052
|
+
}
|
|
17038
17053
|
var RESERVED_SETTINGS_BRANCH_PREFIX3;
|
|
17039
17054
|
var init_branch_name2 = __esm({
|
|
17040
17055
|
"dist/paths/branch-name.js"() {
|
|
@@ -17047,6 +17062,18 @@ var init_branch_name2 = __esm({
|
|
|
17047
17062
|
function isValidContentId2(id) {
|
|
17048
17063
|
return CONTENT_ID_PATTERN3.test(id);
|
|
17049
17064
|
}
|
|
17065
|
+
function isExactBranchForm(param) {
|
|
17066
|
+
return param.startsWith(EXACT_BRANCH_MARK);
|
|
17067
|
+
}
|
|
17068
|
+
function exactBranchSegment(fullName) {
|
|
17069
|
+
return `${EXACT_BRANCH_MARK}${fullName.replaceAll("/", EXACT_BRANCH_MARK)}`;
|
|
17070
|
+
}
|
|
17071
|
+
function exactBranchName(param) {
|
|
17072
|
+
if (!isExactBranchForm(param) || param.includes("/"))
|
|
17073
|
+
return null;
|
|
17074
|
+
const named = parseBranchRef3(param.slice(EXACT_BRANCH_MARK.length).replaceAll(EXACT_BRANCH_MARK, "/"));
|
|
17075
|
+
return named.ok ? named.name : null;
|
|
17076
|
+
}
|
|
17050
17077
|
function parseBranchRef3(name) {
|
|
17051
17078
|
if (!name || typeof name !== "string") {
|
|
17052
17079
|
return { ok: false, error: "Branch name is required" };
|
|
@@ -17114,7 +17141,7 @@ function parseSlug2(slug) {
|
|
|
17114
17141
|
}
|
|
17115
17142
|
return { ok: true, slug: normalized };
|
|
17116
17143
|
}
|
|
17117
|
-
var BASE58_PATTERN3, CONTENT_ID_PATTERN3, PHYSICAL_SEGMENT_PATTERN3, MAX_BRANCH_NAME_LENGTH3;
|
|
17144
|
+
var BASE58_PATTERN3, CONTENT_ID_PATTERN3, PHYSICAL_SEGMENT_PATTERN3, MAX_BRANCH_NAME_LENGTH3, EXACT_BRANCH_MARK;
|
|
17118
17145
|
var init_validation3 = __esm({
|
|
17119
17146
|
"dist/paths/validation.js"() {
|
|
17120
17147
|
"use strict";
|
|
@@ -17122,6 +17149,7 @@ var init_validation3 = __esm({
|
|
|
17122
17149
|
CONTENT_ID_PATTERN3 = new RegExp(`^${BASE58_PATTERN3}{12}$`);
|
|
17123
17150
|
PHYSICAL_SEGMENT_PATTERN3 = new RegExp(`\\.${BASE58_PATTERN3}{12}(?:\\.[a-z]+)?$`);
|
|
17124
17151
|
MAX_BRANCH_NAME_LENGTH3 = 250;
|
|
17152
|
+
EXACT_BRANCH_MARK = "~";
|
|
17125
17153
|
}
|
|
17126
17154
|
});
|
|
17127
17155
|
|
|
@@ -17498,27 +17526,39 @@ var init_operating_mode2 = __esm({
|
|
|
17498
17526
|
}
|
|
17499
17527
|
});
|
|
17500
17528
|
|
|
17501
|
-
// dist/cli/
|
|
17502
|
-
var configured_asset_store_exports = {};
|
|
17503
|
-
__export(configured_asset_store_exports, {
|
|
17504
|
-
loadConfiguredAssetStore: () => loadConfiguredAssetStore
|
|
17505
|
-
});
|
|
17529
|
+
// dist/cli/site-config.js
|
|
17506
17530
|
import path31 from "node:path";
|
|
17507
17531
|
import { createJiti as createJiti3 } from "jiti";
|
|
17508
|
-
|
|
17509
|
-
async function loadConfiguredAssetStore(projectDir) {
|
|
17532
|
+
async function importSiteServerConfig(projectDir) {
|
|
17510
17533
|
const configPath = path31.join(projectDir, "canopycms.config.ts");
|
|
17511
17534
|
const jiti2 = createJiti3(import.meta.url);
|
|
17512
17535
|
const mod = await jiti2.import(configPath);
|
|
17513
17536
|
const configExport = mod.default ?? mod.config ?? mod;
|
|
17514
17537
|
const server = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
17538
|
+
return { configPath, server };
|
|
17539
|
+
}
|
|
17540
|
+
var init_site_config = __esm({
|
|
17541
|
+
"dist/cli/site-config.js"() {
|
|
17542
|
+
"use strict";
|
|
17543
|
+
}
|
|
17544
|
+
});
|
|
17545
|
+
|
|
17546
|
+
// dist/cli/configured-asset-store.js
|
|
17547
|
+
var configured_asset_store_exports = {};
|
|
17548
|
+
__export(configured_asset_store_exports, {
|
|
17549
|
+
loadConfiguredAssetStore: () => loadConfiguredAssetStore
|
|
17550
|
+
});
|
|
17551
|
+
import path33 from "node:path";
|
|
17552
|
+
import { z as z15 } from "zod";
|
|
17553
|
+
async function loadConfiguredAssetStore(projectDir) {
|
|
17554
|
+
const { configPath, server } = await importSiteServerConfig(projectDir);
|
|
17515
17555
|
const parsed = serverConfigSchema.safeParse(server);
|
|
17516
17556
|
if (!parsed.success) {
|
|
17517
17557
|
throw new Error(`Invalid CanopyCMS config at ${configPath}: ${parsed.error.message}`);
|
|
17518
17558
|
}
|
|
17519
17559
|
const { mode, media } = parsed.data;
|
|
17520
|
-
const devAssetsDir = mode === "dev" ?
|
|
17521
|
-
const resolvedMedia = media?.adapter === "local" && media.directory ? { ...media, directory:
|
|
17560
|
+
const devAssetsDir = mode === "dev" ? path33.join(operatingStrategy3(mode).getWorkspaceRoot(projectDir), "assets") : void 0;
|
|
17561
|
+
const resolvedMedia = media?.adapter === "local" && media.directory ? { ...media, directory: path33.resolve(projectDir, media.directory) } : media;
|
|
17522
17562
|
const store = createAssetStore(resolvedMedia, { devAssetsDir });
|
|
17523
17563
|
if (!store) {
|
|
17524
17564
|
throw new Error(`No asset store is configured in ${configPath}: set \`media\` (an s3 adapter in prod).`);
|
|
@@ -17532,6 +17572,7 @@ var init_configured_asset_store = __esm({
|
|
|
17532
17572
|
init_factory();
|
|
17533
17573
|
init_media3();
|
|
17534
17574
|
init_operating_mode2();
|
|
17575
|
+
init_site_config();
|
|
17535
17576
|
serverConfigSchema = z15.object({
|
|
17536
17577
|
mode: z15.enum(["prod", "dev"]),
|
|
17537
17578
|
media: mediaSchema3.optional()
|
|
@@ -17547,13 +17588,13 @@ __export(asset_refs_exports, {
|
|
|
17547
17588
|
materializeAssetsCLI: () => materializeAssetsCLI
|
|
17548
17589
|
});
|
|
17549
17590
|
import fs27 from "node:fs/promises";
|
|
17550
|
-
import
|
|
17591
|
+
import path34 from "node:path";
|
|
17551
17592
|
async function collectAssetRefsCLI(options) {
|
|
17552
17593
|
if (!options.outDir) {
|
|
17553
17594
|
console.error("Usage: canopycms collect-asset-refs <outDir>");
|
|
17554
17595
|
return 1;
|
|
17555
17596
|
}
|
|
17556
|
-
const outDir =
|
|
17597
|
+
const outDir = path34.resolve(options.outDir);
|
|
17557
17598
|
try {
|
|
17558
17599
|
if (!(await fs27.stat(outDir)).isDirectory()) {
|
|
17559
17600
|
console.error(`Not a directory: ${outDir}`);
|
|
@@ -17648,19 +17689,19 @@ async function runMaterialize(options) {
|
|
|
17648
17689
|
console.error("canopycms materialize-assets: --report needs a file");
|
|
17649
17690
|
return MATERIALIZE_EXIT_CODES.error;
|
|
17650
17691
|
}
|
|
17651
|
-
if (options.reportPath &&
|
|
17692
|
+
if (options.reportPath && path34.resolve(options.reportPath) === path34.resolve(options.refsPath)) {
|
|
17652
17693
|
console.error("canopycms materialize-assets: --report must not be the --refs file");
|
|
17653
17694
|
return MATERIALIZE_EXIT_CODES.error;
|
|
17654
17695
|
}
|
|
17655
17696
|
if (options.reportPath)
|
|
17656
|
-
await fs27.rm(
|
|
17697
|
+
await fs27.rm(path34.resolve(options.reportPath), { force: true });
|
|
17657
17698
|
const concurrency = parsePositiveInteger("--concurrency", options.concurrency);
|
|
17658
17699
|
const transformConcurrency = parsePositiveInteger("--transform-concurrency", options.transformConcurrency);
|
|
17659
17700
|
if (concurrency === null || transformConcurrency === null)
|
|
17660
17701
|
return MATERIALIZE_EXIT_CODES.error;
|
|
17661
17702
|
if (options.outputPrefix !== void 0)
|
|
17662
17703
|
assertValidOutputPrefix(options.outputPrefix);
|
|
17663
|
-
const refs = await readAssetRefsFile(
|
|
17704
|
+
const refs = await readAssetRefsFile(path34.resolve(options.refsPath));
|
|
17664
17705
|
const store = await resolveStore(options);
|
|
17665
17706
|
if (!store)
|
|
17666
17707
|
return MATERIALIZE_EXIT_CODES.error;
|
|
@@ -17674,7 +17715,7 @@ async function runMaterialize(options) {
|
|
|
17674
17715
|
});
|
|
17675
17716
|
printReport(report);
|
|
17676
17717
|
if (options.reportPath) {
|
|
17677
|
-
await atomicWriteFile3(
|
|
17718
|
+
await atomicWriteFile3(path34.resolve(options.reportPath), `${JSON.stringify(report, null, 2)}
|
|
17678
17719
|
`);
|
|
17679
17720
|
}
|
|
17680
17721
|
const { contentFailures, storeFailures } = report.summary;
|
|
@@ -17714,20 +17755,149 @@ var init_asset_refs2 = __esm({
|
|
|
17714
17755
|
}
|
|
17715
17756
|
});
|
|
17716
17757
|
|
|
17717
|
-
// dist/
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17758
|
+
// dist/branch-metadata-error.js
|
|
17759
|
+
var BranchMetadataCorruptError2;
|
|
17760
|
+
var init_branch_metadata_error = __esm({
|
|
17761
|
+
"dist/branch-metadata-error.js"() {
|
|
17762
|
+
"use strict";
|
|
17763
|
+
BranchMetadataCorruptError2 = class extends Error {
|
|
17764
|
+
constructor(branchRoot, cause) {
|
|
17765
|
+
super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
|
|
17766
|
+
this.name = "BranchMetadataCorruptError";
|
|
17767
|
+
this.branchRoot = branchRoot;
|
|
17768
|
+
this.parseCause = cause;
|
|
17769
|
+
}
|
|
17770
|
+
};
|
|
17771
|
+
}
|
|
17772
|
+
});
|
|
17773
|
+
|
|
17774
|
+
// dist/branch-metadata-file.js
|
|
17775
|
+
import fs28 from "node:fs/promises";
|
|
17776
|
+
import path35 from "node:path";
|
|
17777
|
+
import { z as z16 } from "zod";
|
|
17778
|
+
function describeSchemaIssues2(issues) {
|
|
17779
|
+
const missing = /* @__PURE__ */ new Set();
|
|
17780
|
+
const invalid = /* @__PURE__ */ new Set();
|
|
17781
|
+
for (const issue of issues) {
|
|
17782
|
+
const field = issue.path.length > 0 ? issue.path.join(".") : "the file";
|
|
17783
|
+
const isMissing = issue.code === z16.ZodIssueCode.invalid_type && issue.received === z16.ZodParsedType.undefined;
|
|
17784
|
+
(isMissing ? missing : invalid).add(field);
|
|
17785
|
+
}
|
|
17786
|
+
const parts = [];
|
|
17787
|
+
if (missing.size > 0)
|
|
17788
|
+
parts.push(`Missing: ${[...missing].join(", ")}`);
|
|
17789
|
+
if (invalid.size > 0)
|
|
17790
|
+
parts.push(`Invalid: ${[...invalid].join(", ")}`);
|
|
17791
|
+
return `Not branch metadata. ${parts.join(". ")}`;
|
|
17721
17792
|
}
|
|
17722
|
-
async function
|
|
17723
|
-
|
|
17724
|
-
|
|
17793
|
+
async function readBranchMetadataFile2(branchRoot) {
|
|
17794
|
+
const resolvedRoot = path35.resolve(branchRoot);
|
|
17795
|
+
let raw;
|
|
17796
|
+
try {
|
|
17797
|
+
raw = await fs28.readFile(branchMetadataFilePath2(resolvedRoot), "utf8");
|
|
17798
|
+
} catch (err2) {
|
|
17799
|
+
if (isNotFoundError(err2)) {
|
|
17800
|
+
return null;
|
|
17801
|
+
}
|
|
17802
|
+
throw err2;
|
|
17803
|
+
}
|
|
17804
|
+
return parseBranchMetadataFile2(resolvedRoot, raw);
|
|
17725
17805
|
}
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17806
|
+
function parseBranchMetadataFile2(branchRoot, raw) {
|
|
17807
|
+
let json;
|
|
17808
|
+
try {
|
|
17809
|
+
json = JSON.parse(raw);
|
|
17810
|
+
} catch (err2) {
|
|
17811
|
+
throw new BranchMetadataCorruptError2(branchRoot, `Not valid JSON: ${getErrorMessage(err2)}`);
|
|
17812
|
+
}
|
|
17813
|
+
const result = branchMetadataFileSchema2.safeParse(json);
|
|
17814
|
+
if (!result.success) {
|
|
17815
|
+
throw new BranchMetadataCorruptError2(branchRoot, describeSchemaIssues2(result.error.issues));
|
|
17816
|
+
}
|
|
17817
|
+
const file = result.data;
|
|
17818
|
+
return file;
|
|
17819
|
+
}
|
|
17820
|
+
var BRANCH_META_DIR2, BRANCH_META_FILE2, branchMetadataFilePath2, UNKNOWN_TIME2, enumOf2, branchMetadataShape2, branchMetadataSchema2, branchMetadataFileSchema2;
|
|
17821
|
+
var init_branch_metadata_file = __esm({
|
|
17822
|
+
"dist/branch-metadata-file.js"() {
|
|
17729
17823
|
"use strict";
|
|
17730
|
-
|
|
17824
|
+
init_error();
|
|
17825
|
+
init_branch_metadata_error();
|
|
17826
|
+
BRANCH_META_DIR2 = ".canopy-meta";
|
|
17827
|
+
BRANCH_META_FILE2 = "branch.json";
|
|
17828
|
+
branchMetadataFilePath2 = (branchRoot) => path35.join(path35.resolve(branchRoot), BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
17829
|
+
UNKNOWN_TIME2 = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
17830
|
+
enumOf2 = (values) => z16.nativeEnum(values);
|
|
17831
|
+
branchMetadataShape2 = {
|
|
17832
|
+
name: z16.string().min(1),
|
|
17833
|
+
title: z16.string().optional(),
|
|
17834
|
+
description: z16.string().optional(),
|
|
17835
|
+
status: enumOf2({
|
|
17836
|
+
editing: "editing",
|
|
17837
|
+
submitted: "submitted",
|
|
17838
|
+
approved: "approved",
|
|
17839
|
+
archived: "archived"
|
|
17840
|
+
}),
|
|
17841
|
+
access: z16.object({
|
|
17842
|
+
allowedUsers: z16.array(z16.string()).optional(),
|
|
17843
|
+
allowedGroups: z16.array(z16.string()).optional(),
|
|
17844
|
+
managerOrAdminAllowed: z16.boolean().optional()
|
|
17845
|
+
}).passthrough(),
|
|
17846
|
+
createdBy: z16.string().default("unknown"),
|
|
17847
|
+
createdAt: z16.string().default(UNKNOWN_TIME2),
|
|
17848
|
+
updatedAt: z16.string().default(UNKNOWN_TIME2),
|
|
17849
|
+
baseBranch: z16.string().optional(),
|
|
17850
|
+
pullRequestUrl: z16.string().optional(),
|
|
17851
|
+
pullRequestNumber: z16.number().int().optional(),
|
|
17852
|
+
submittedAt: z16.string().optional(),
|
|
17853
|
+
pushedToGitHubAt: z16.string().optional(),
|
|
17854
|
+
editorUrl: z16.string().optional(),
|
|
17855
|
+
syncStatus: enumOf2({
|
|
17856
|
+
synced: "synced",
|
|
17857
|
+
"pending-sync": "pending-sync",
|
|
17858
|
+
"sync-failed": "sync-failed"
|
|
17859
|
+
}).optional(),
|
|
17860
|
+
conflictStatus: enumOf2({
|
|
17861
|
+
clean: "clean",
|
|
17862
|
+
"conflicts-detected": "conflicts-detected"
|
|
17863
|
+
}).optional(),
|
|
17864
|
+
conflictFiles: z16.array(z16.custom((v) => typeof v === "string")).optional(),
|
|
17865
|
+
pullRequestState: enumOf2({
|
|
17866
|
+
open: "open",
|
|
17867
|
+
closed: "closed",
|
|
17868
|
+
merged: "merged"
|
|
17869
|
+
}).optional(),
|
|
17870
|
+
mergedAt: z16.string().optional(),
|
|
17871
|
+
rebaseFailure: z16.object({ message: z16.string(), firstAt: z16.string(), lastAt: z16.string() }).optional(),
|
|
17872
|
+
historyRewrittenFrom: z16.string().optional(),
|
|
17873
|
+
lastGitHubTip: z16.string().optional(),
|
|
17874
|
+
keptOverGitHub: z16.array(z16.string()).optional(),
|
|
17875
|
+
rewrittenOnGitHub: z16.string().optional(),
|
|
17876
|
+
submitUnfinished: z16.boolean().optional(),
|
|
17877
|
+
submitNotQueued: z16.boolean().optional(),
|
|
17878
|
+
unpublishedAtMerge: z16.boolean().optional(),
|
|
17879
|
+
syncFailureReason: z16.string().optional(),
|
|
17880
|
+
editors: z16.array(z16.string()).optional(),
|
|
17881
|
+
uncommittedEditors: z16.array(z16.string()).optional(),
|
|
17882
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
17883
|
+
pullRequestHold: z16.discriminatedUnion("kind", [
|
|
17884
|
+
z16.object({ kind: z16.literal("conflicts") }),
|
|
17885
|
+
z16.object({ kind: z16.literal("retargeted"), base: z16.string() })
|
|
17886
|
+
]).optional().catch(void 0),
|
|
17887
|
+
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
17888
|
+
review: z16.object({
|
|
17889
|
+
state: z16.enum(["approved", "changes-requested"]),
|
|
17890
|
+
by: z16.object({ userId: z16.string(), name: z16.string().optional() }),
|
|
17891
|
+
at: z16.string()
|
|
17892
|
+
}).optional().catch(void 0)
|
|
17893
|
+
};
|
|
17894
|
+
branchMetadataSchema2 = z16.object(branchMetadataShape2).passthrough();
|
|
17895
|
+
branchMetadataFileSchema2 = z16.object({
|
|
17896
|
+
schemaVersion: z16.number().int().default(1),
|
|
17897
|
+
version: z16.number().int().nonnegative().default(0),
|
|
17898
|
+
writeId: z16.string().optional(),
|
|
17899
|
+
branch: branchMetadataSchema2
|
|
17900
|
+
}).passthrough();
|
|
17731
17901
|
}
|
|
17732
17902
|
});
|
|
17733
17903
|
|
|
@@ -17836,11 +18006,11 @@ var init_debug2 = __esm({
|
|
|
17836
18006
|
});
|
|
17837
18007
|
|
|
17838
18008
|
// dist/resource-generation.js
|
|
17839
|
-
import
|
|
17840
|
-
import
|
|
18009
|
+
import fs29 from "node:fs/promises";
|
|
18010
|
+
import path36 from "node:path";
|
|
17841
18011
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
17842
18012
|
function resourceGenerationPath2(root, resource) {
|
|
17843
|
-
return
|
|
18013
|
+
return path36.resolve(root, META_DIR3, `${resource}.generation`);
|
|
17844
18014
|
}
|
|
17845
18015
|
async function bumpResourceGeneration2(root, resource, options) {
|
|
17846
18016
|
const token = randomUUID5();
|
|
@@ -17848,9 +18018,7 @@ async function bumpResourceGeneration2(root, resource, options) {
|
|
|
17848
18018
|
await atomicWriteFile3(resourceGenerationPath2(root, resource), token);
|
|
17849
18019
|
return token;
|
|
17850
18020
|
} catch (err2) {
|
|
17851
|
-
|
|
17852
|
-
error: getErrorMessage(err2)
|
|
17853
|
-
});
|
|
18021
|
+
canopyLogWarn3(`CanopyCMS: failed to bump ${resource} generation for ${root}:`, getErrorMessage(err2));
|
|
17854
18022
|
if (options?.mustSucceed)
|
|
17855
18023
|
throw err2;
|
|
17856
18024
|
return null;
|
|
@@ -17858,7 +18026,7 @@ async function bumpResourceGeneration2(root, resource, options) {
|
|
|
17858
18026
|
}
|
|
17859
18027
|
async function readResourceGeneration2(root, resource) {
|
|
17860
18028
|
try {
|
|
17861
|
-
const token = await
|
|
18029
|
+
const token = await fs29.readFile(resourceGenerationPath2(root, resource), "utf-8");
|
|
17862
18030
|
return { ok: true, token };
|
|
17863
18031
|
} catch (err2) {
|
|
17864
18032
|
if (isNodeError(err2) && err2.code === "ENOENT")
|
|
@@ -17879,275 +18047,320 @@ var init_resource_generation = __esm({
|
|
|
17879
18047
|
init_atomic_write3();
|
|
17880
18048
|
init_error();
|
|
17881
18049
|
init_debug2();
|
|
18050
|
+
init_logger3();
|
|
17882
18051
|
log11 = createDebugLogger3({ prefix: "ResourceGeneration" });
|
|
17883
18052
|
META_DIR3 = ".canopy-meta";
|
|
17884
18053
|
}
|
|
17885
18054
|
});
|
|
17886
18055
|
|
|
17887
|
-
// dist/
|
|
17888
|
-
import
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
continue;
|
|
17895
|
-
for (const ref of refs) {
|
|
17896
|
-
const target = ref.deref();
|
|
17897
|
-
if (target) {
|
|
17898
|
-
target.invalidateIndex();
|
|
17899
|
-
} else {
|
|
17900
|
-
refs.delete(ref);
|
|
17901
|
-
}
|
|
17902
|
-
}
|
|
17903
|
-
if (refs.size === 0)
|
|
17904
|
-
registry2.delete(storeRoot);
|
|
17905
|
-
}
|
|
17906
|
-
}
|
|
17907
|
-
var registry2, finalization2;
|
|
17908
|
-
var init_content_index_registry = __esm({
|
|
17909
|
-
"dist/content-index-registry.js"() {
|
|
18056
|
+
// dist/branch-registry.js
|
|
18057
|
+
import fs30 from "node:fs/promises";
|
|
18058
|
+
import path37 from "node:path";
|
|
18059
|
+
import { z as z17 } from "zod";
|
|
18060
|
+
var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE3, GET_MISS_REFRESH_MIN_INTERVAL_MS2, snapshotSchema2, reportedUnreadable2, BranchRegistry2;
|
|
18061
|
+
var init_branch_registry = __esm({
|
|
18062
|
+
"dist/branch-registry.js"() {
|
|
17910
18063
|
"use strict";
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
18064
|
+
init_branch_metadata_file();
|
|
18065
|
+
init_error();
|
|
18066
|
+
init_debug2();
|
|
18067
|
+
init_logger3();
|
|
18068
|
+
init_resource_generation();
|
|
18069
|
+
log12 = createDebugLogger3({ prefix: "BranchRegistry" });
|
|
18070
|
+
REGISTRY_FILE2 = "branches.json";
|
|
18071
|
+
REGISTRY_STALE_FILE2 = "branches.stale.json";
|
|
18072
|
+
REGISTRY_TEMP_FILE2 = "branches.tmp.json";
|
|
18073
|
+
REGISTRY_VERSION2 = 2;
|
|
18074
|
+
RESOURCE3 = "branch-registry";
|
|
18075
|
+
GET_MISS_REFRESH_MIN_INTERVAL_MS2 = 5e3;
|
|
18076
|
+
snapshotSchema2 = z17.object({
|
|
18077
|
+
version: z17.literal(REGISTRY_VERSION2),
|
|
18078
|
+
branches: z17.array(z17.object({ branch: branchMetadataSchema2, branchRoot: z17.string(), baseRoot: z17.string() })),
|
|
18079
|
+
generation: z17.string().nullable()
|
|
17919
18080
|
});
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
var isValidId2;
|
|
17930
|
-
var init_id = __esm({
|
|
17931
|
-
"dist/id.js"() {
|
|
17932
|
-
"use strict";
|
|
17933
|
-
init_validation3();
|
|
17934
|
-
isValidId2 = isValidContentId2;
|
|
17935
|
-
}
|
|
17936
|
-
});
|
|
17937
|
-
|
|
17938
|
-
// dist/utils/format.js
|
|
17939
|
-
var getFormatExtension2;
|
|
17940
|
-
var init_format = __esm({
|
|
17941
|
-
"dist/utils/format.js"() {
|
|
17942
|
-
"use strict";
|
|
17943
|
-
getFormatExtension2 = (format) => {
|
|
17944
|
-
if (format === "md")
|
|
17945
|
-
return ".md";
|
|
17946
|
-
if (format === "mdx")
|
|
17947
|
-
return ".mdx";
|
|
17948
|
-
if (format === "yaml")
|
|
17949
|
-
return ".yaml";
|
|
17950
|
-
return ".json";
|
|
17951
|
-
};
|
|
17952
|
-
}
|
|
17953
|
-
});
|
|
17954
|
-
|
|
17955
|
-
// dist/content-id-index.js
|
|
17956
|
-
function extractIdFromFilename2(filename) {
|
|
17957
|
-
if (filename.startsWith(".")) {
|
|
17958
|
-
return null;
|
|
17959
|
-
}
|
|
17960
|
-
const parts = filename.split(".");
|
|
17961
|
-
if (parts.length >= 3) {
|
|
17962
|
-
const candidate = parts[parts.length - 2];
|
|
17963
|
-
if (isValidId2(candidate))
|
|
17964
|
-
return candidate;
|
|
17965
|
-
}
|
|
17966
|
-
if (parts.length === 2) {
|
|
17967
|
-
const candidate = parts[parts.length - 1];
|
|
17968
|
-
if (isValidId2(candidate))
|
|
17969
|
-
return candidate;
|
|
17970
|
-
}
|
|
17971
|
-
return null;
|
|
17972
|
-
}
|
|
17973
|
-
function extractSlugFromFilename2(filename, entryTypeName) {
|
|
17974
|
-
const parts = filename.split(".");
|
|
17975
|
-
if (parts.length >= 3) {
|
|
17976
|
-
const possibleId = parts[parts.length - 2];
|
|
17977
|
-
if (isValidId2(possibleId)) {
|
|
17978
|
-
let slugParts = parts.slice(0, parts.length - 2);
|
|
17979
|
-
if (entryTypeName && slugParts.length > 1 && slugParts[0] === entryTypeName) {
|
|
17980
|
-
slugParts = slugParts.slice(1);
|
|
17981
|
-
} else if (parts.length >= 4 && slugParts.length > 1) {
|
|
17982
|
-
slugParts = slugParts.slice(1);
|
|
18081
|
+
reportedUnreadable2 = /* @__PURE__ */ new Set();
|
|
18082
|
+
BranchRegistry2 = class {
|
|
18083
|
+
constructor(root) {
|
|
18084
|
+
this.regenInFlight = null;
|
|
18085
|
+
this.lastForcedRefreshMs = 0;
|
|
18086
|
+
this.root = path37.resolve(root);
|
|
18087
|
+
this.registryPath = path37.join(this.root, REGISTRY_FILE2);
|
|
18088
|
+
this.stalePath = path37.join(this.root, REGISTRY_STALE_FILE2);
|
|
18089
|
+
this.tempPath = path37.join(this.root, REGISTRY_TEMP_FILE2);
|
|
17983
18090
|
}
|
|
17984
|
-
|
|
17985
|
-
|
|
18091
|
+
/**
|
|
18092
|
+
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
18093
|
+
* token matches the live marker, regenerates otherwise.
|
|
18094
|
+
*
|
|
18095
|
+
* An unparseable or wrong-shaped snapshot is treated like an absent one: the
|
|
18096
|
+
* snapshot is a cache of the branch directories, so rebuilding it loses
|
|
18097
|
+
* nothing, while rethrowing would fail every editor's branch listing until
|
|
18098
|
+
* someone deleted the file. Other read failures (permissions, IO) propagate.
|
|
18099
|
+
*/
|
|
18100
|
+
async list() {
|
|
18101
|
+
let raw;
|
|
18102
|
+
try {
|
|
18103
|
+
raw = await fs30.readFile(this.registryPath, "utf8");
|
|
18104
|
+
} catch (err2) {
|
|
18105
|
+
if (isNotFoundError(err2)) {
|
|
18106
|
+
return await this.regenerate();
|
|
18107
|
+
}
|
|
18108
|
+
throw err2;
|
|
18109
|
+
}
|
|
18110
|
+
const parsed = this.parseSnapshot(raw);
|
|
18111
|
+
if (!parsed)
|
|
18112
|
+
return await this.regenerate();
|
|
18113
|
+
const read = await readResourceGeneration2(this.root, RESOURCE3);
|
|
18114
|
+
if (isGenerationCurrent2(parsed.generation, read)) {
|
|
18115
|
+
return parsed.branches;
|
|
18116
|
+
}
|
|
18117
|
+
return await this.regenerate();
|
|
18118
|
+
}
|
|
18119
|
+
/** The snapshot in `raw`, or null when it must be rebuilt. */
|
|
18120
|
+
parseSnapshot(raw) {
|
|
18121
|
+
let json;
|
|
18122
|
+
try {
|
|
18123
|
+
json = JSON.parse(raw);
|
|
18124
|
+
} catch (err2) {
|
|
18125
|
+
this.reportUnreadable(getErrorMessage(err2));
|
|
18126
|
+
return null;
|
|
18127
|
+
}
|
|
18128
|
+
if (typeof json === "object" && json !== null && "version" in json && typeof json.version === "number" && json.version !== REGISTRY_VERSION2) {
|
|
18129
|
+
return null;
|
|
18130
|
+
}
|
|
18131
|
+
const result = snapshotSchema2.safeParse(json);
|
|
18132
|
+
if (!result.success) {
|
|
18133
|
+
this.reportUnreadable("it does not have the registry snapshot shape");
|
|
18134
|
+
return null;
|
|
18135
|
+
}
|
|
18136
|
+
reportedUnreadable2.delete(this.registryPath);
|
|
18137
|
+
const snapshot = result.data;
|
|
18138
|
+
return snapshot;
|
|
18139
|
+
}
|
|
18140
|
+
reportUnreadable(reason) {
|
|
18141
|
+
if (reportedUnreadable2.has(this.registryPath))
|
|
18142
|
+
return;
|
|
18143
|
+
reportedUnreadable2.add(this.registryPath);
|
|
18144
|
+
canopyLogWarn3(`CanopyCMS: Branch registry snapshot ${this.registryPath} is unreadable (${reason}); regenerating it from the branch directories`);
|
|
18145
|
+
}
|
|
18146
|
+
/**
|
|
18147
|
+
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
18148
|
+
*
|
|
18149
|
+
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
18150
|
+
* the snapshot may predate a branch that now exists, so force one fresh
|
|
18151
|
+
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
18152
|
+
* genuinely-absent branch costs at most one extra scan per window.
|
|
18153
|
+
*/
|
|
18154
|
+
async get(name) {
|
|
18155
|
+
return this.find((b) => b.branch.name === name);
|
|
18156
|
+
}
|
|
18157
|
+
/**
|
|
18158
|
+
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
18159
|
+
* whatever its name, with {@link get}'s backstop.
|
|
18160
|
+
*/
|
|
18161
|
+
async getByKey(key) {
|
|
18162
|
+
return this.find((b) => path37.basename(b.branchRoot) === key);
|
|
18163
|
+
}
|
|
18164
|
+
async find(matches) {
|
|
18165
|
+
const found = (await this.list()).find(matches);
|
|
18166
|
+
if (found)
|
|
18167
|
+
return found;
|
|
18168
|
+
if (!this.shouldForceRefresh())
|
|
18169
|
+
return void 0;
|
|
18170
|
+
const refreshed = await this.regenerate();
|
|
18171
|
+
return refreshed.find(matches);
|
|
18172
|
+
}
|
|
18173
|
+
shouldForceRefresh() {
|
|
18174
|
+
const now = Date.now();
|
|
18175
|
+
if (now - this.lastForcedRefreshMs < GET_MISS_REFRESH_MIN_INTERVAL_MS2)
|
|
18176
|
+
return false;
|
|
18177
|
+
this.lastForcedRefreshMs = now;
|
|
18178
|
+
return true;
|
|
18179
|
+
}
|
|
18180
|
+
/**
|
|
18181
|
+
* Marks the cache stale for every process sharing this root by bumping the
|
|
18182
|
+
* marker, then eager-regenerates on this host. The bump must succeed —
|
|
18183
|
+
* swallowing that failure leaves the registry stale indefinitely with no
|
|
18184
|
+
* backstop. A failed eager regen must NOT fail the caller's save/delete: the
|
|
18185
|
+
* bump alone already restored correctness for every future reader.
|
|
18186
|
+
*/
|
|
18187
|
+
async invalidate() {
|
|
18188
|
+
await bumpResourceGeneration2(this.root, RESOURCE3, { mustSucceed: true });
|
|
18189
|
+
try {
|
|
18190
|
+
if (this.regenInFlight)
|
|
18191
|
+
await this.regenInFlight.catch(() => {
|
|
18192
|
+
});
|
|
18193
|
+
await this.regenerate();
|
|
18194
|
+
} catch (err2) {
|
|
18195
|
+
log12.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
18196
|
+
error: getErrorMessage(err2)
|
|
18197
|
+
});
|
|
18198
|
+
}
|
|
18199
|
+
}
|
|
18200
|
+
/**
|
|
18201
|
+
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
18202
|
+
* callers on this instance to a single underlying scan. Never loops waiting
|
|
18203
|
+
* for the embedded token to match the marker — that livelocks under a bump
|
|
18204
|
+
* storm; a caller wanting the very latest state calls list() again.
|
|
18205
|
+
*/
|
|
18206
|
+
async regenerate() {
|
|
18207
|
+
if (this.regenInFlight)
|
|
18208
|
+
return this.regenInFlight;
|
|
18209
|
+
const run = this.performRegenerate();
|
|
18210
|
+
this.regenInFlight = run;
|
|
18211
|
+
try {
|
|
18212
|
+
return await run;
|
|
18213
|
+
} finally {
|
|
18214
|
+
this.regenInFlight = null;
|
|
18215
|
+
}
|
|
18216
|
+
}
|
|
18217
|
+
async performRegenerate() {
|
|
18218
|
+
const read = await readResourceGeneration2(this.root, RESOURCE3);
|
|
18219
|
+
const branches = await this.scanBranchDirectories();
|
|
18220
|
+
await fs30.unlink(this.stalePath).catch(() => {
|
|
18221
|
+
});
|
|
18222
|
+
if (!read.ok) {
|
|
18223
|
+
return branches;
|
|
18224
|
+
}
|
|
18225
|
+
const snapshot = {
|
|
18226
|
+
version: REGISTRY_VERSION2,
|
|
18227
|
+
branches,
|
|
18228
|
+
generation: read.token
|
|
18229
|
+
};
|
|
18230
|
+
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
18231
|
+
await fs30.mkdir(this.root, { recursive: true });
|
|
18232
|
+
await fs30.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
18233
|
+
try {
|
|
18234
|
+
await fs30.rename(uniqueTempPath, this.registryPath);
|
|
18235
|
+
} catch (err2) {
|
|
18236
|
+
await fs30.unlink(uniqueTempPath).catch(() => {
|
|
18237
|
+
});
|
|
18238
|
+
throw err2;
|
|
18239
|
+
}
|
|
18240
|
+
return branches;
|
|
18241
|
+
}
|
|
18242
|
+
/**
|
|
18243
|
+
* Scans the root directory for branch subdirectories with valid branch.json files.
|
|
18244
|
+
* Protected (rather than exported as a test hook) so tests can subclass and
|
|
18245
|
+
* override to simulate cross-process interleavings.
|
|
18246
|
+
*/
|
|
18247
|
+
async scanBranchDirectories() {
|
|
18248
|
+
const branches = [];
|
|
18249
|
+
try {
|
|
18250
|
+
const entries = await fs30.readdir(this.root, { withFileTypes: true });
|
|
18251
|
+
for (const entry of entries) {
|
|
18252
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
18253
|
+
continue;
|
|
18254
|
+
}
|
|
18255
|
+
const branchRoot = path37.join(this.root, entry.name);
|
|
18256
|
+
let meta;
|
|
18257
|
+
try {
|
|
18258
|
+
meta = await readBranchMetadataFile2(branchRoot);
|
|
18259
|
+
} catch (err2) {
|
|
18260
|
+
canopyLogWarn3(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err2)}`);
|
|
18261
|
+
continue;
|
|
18262
|
+
}
|
|
18263
|
+
if (meta) {
|
|
18264
|
+
branches.push({
|
|
18265
|
+
branch: meta.branch,
|
|
18266
|
+
branchRoot,
|
|
18267
|
+
baseRoot: this.root
|
|
18268
|
+
});
|
|
18269
|
+
}
|
|
18270
|
+
}
|
|
18271
|
+
} catch (err2) {
|
|
18272
|
+
if (isNotFoundError(err2)) {
|
|
18273
|
+
return [];
|
|
18274
|
+
}
|
|
18275
|
+
throw err2;
|
|
18276
|
+
}
|
|
18277
|
+
return branches;
|
|
18278
|
+
}
|
|
18279
|
+
};
|
|
17986
18280
|
}
|
|
17987
|
-
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
18281
|
+
});
|
|
18282
|
+
|
|
18283
|
+
// dist/paths/branch.js
|
|
18284
|
+
import path38 from "node:path";
|
|
18285
|
+
function namesNoWorkspace(err2) {
|
|
18286
|
+
if (err2 instanceof BranchPathError2)
|
|
18287
|
+
return true;
|
|
18288
|
+
return isNodeError(err2) && (err2.code === "ENAMETOOLONG" || err2.code === "ENOTDIR");
|
|
18289
|
+
}
|
|
18290
|
+
function resolveBranchPath2(options) {
|
|
18291
|
+
if (options.branchName.includes("..")) {
|
|
18292
|
+
throw new BranchPathError2("Branch name cannot contain traversal segments");
|
|
17992
18293
|
}
|
|
17993
|
-
if (
|
|
17994
|
-
|
|
18294
|
+
if (isSettingsBranchName2(options.branchName)) {
|
|
18295
|
+
throw new BranchPathError2("Settings branches are not content branches");
|
|
17995
18296
|
}
|
|
17996
|
-
|
|
18297
|
+
const safeBranch = sanitizeBranchName3(options.branchName);
|
|
18298
|
+
const strategy = operatingStrategy3(options.mode);
|
|
18299
|
+
const baseRoot = resolveContentBranchesRoot2(options.mode, options.basePathOverride);
|
|
18300
|
+
const normalizedBase = path38.resolve(baseRoot);
|
|
18301
|
+
const baseWithSep = normalizedBase.endsWith(path38.sep) ? normalizedBase : `${normalizedBase}${path38.sep}`;
|
|
18302
|
+
const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
|
|
18303
|
+
const withinBase = (target) => {
|
|
18304
|
+
const resolved = path38.resolve(target);
|
|
18305
|
+
return resolved === normalizedBase || resolved.startsWith(baseWithSep);
|
|
18306
|
+
};
|
|
18307
|
+
if (!withinBase(branchRoot)) {
|
|
18308
|
+
throw new BranchPathError2("Branch path resolves outside the base root");
|
|
18309
|
+
}
|
|
18310
|
+
return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
|
|
17997
18311
|
}
|
|
17998
|
-
var
|
|
17999
|
-
var
|
|
18000
|
-
"dist/
|
|
18312
|
+
var BranchPathError2, resolveContentBranchesRoot2;
|
|
18313
|
+
var init_branch = __esm({
|
|
18314
|
+
"dist/paths/branch.js"() {
|
|
18001
18315
|
"use strict";
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18316
|
+
init_operating_mode2();
|
|
18317
|
+
init_error();
|
|
18318
|
+
init_branch_name2();
|
|
18319
|
+
BranchPathError2 = class extends Error {
|
|
18320
|
+
};
|
|
18321
|
+
resolveContentBranchesRoot2 = (mode, override) => {
|
|
18322
|
+
return operatingStrategy3(mode).getContentBranchesRoot(override);
|
|
18323
|
+
};
|
|
18005
18324
|
}
|
|
18006
18325
|
});
|
|
18007
18326
|
|
|
18008
|
-
// dist/
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
}
|
|
18016
|
-
|
|
18017
|
-
|
|
18327
|
+
// dist/paths/index.js
|
|
18328
|
+
var init_paths = __esm({
|
|
18329
|
+
"dist/paths/index.js"() {
|
|
18330
|
+
"use strict";
|
|
18331
|
+
init_validation3();
|
|
18332
|
+
init_branch();
|
|
18333
|
+
}
|
|
18334
|
+
});
|
|
18335
|
+
|
|
18336
|
+
// dist/utils/async-mutex.js
|
|
18337
|
+
async function withLock2(key, fn) {
|
|
18338
|
+
const prev = locks2.get(key) ?? Promise.resolve();
|
|
18339
|
+
let resolve2;
|
|
18340
|
+
const next = new Promise((r) => {
|
|
18341
|
+
resolve2 = r;
|
|
18342
|
+
});
|
|
18343
|
+
locks2.set(key, next);
|
|
18344
|
+
await prev;
|
|
18018
18345
|
try {
|
|
18019
|
-
|
|
18020
|
-
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
const logicalName = stripEmbeddedIdFromName2(folderName);
|
|
18025
|
-
const collectionContentId = extractIdFromFilename2(folderName) ?? void 0;
|
|
18026
|
-
const folderPath = relativePath ? `${relativePath}/${logicalName}` : logicalName;
|
|
18027
|
-
const absolutePath = join3(baseDir, folderName);
|
|
18028
|
-
const metaPath = join3(absolutePath, ".collection.json");
|
|
18029
|
-
try {
|
|
18030
|
-
await fs29.access(metaPath);
|
|
18031
|
-
const content = await fs29.readFile(metaPath, "utf-8");
|
|
18032
|
-
const parsed = JSON.parse(content);
|
|
18033
|
-
const meta = collectionMetaSchema2.parse(parsed);
|
|
18034
|
-
collections.push({
|
|
18035
|
-
...meta,
|
|
18036
|
-
path: folderPath,
|
|
18037
|
-
contentId: collectionContentId
|
|
18038
|
-
});
|
|
18039
|
-
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
18040
|
-
collections.push(...nestedCollections);
|
|
18041
|
-
} catch (err2) {
|
|
18042
|
-
if (err2.code !== "ENOENT") {
|
|
18043
|
-
canopyLogError2(`Error loading ${metaPath}:`, err2);
|
|
18044
|
-
throw new Error(`Invalid .collection.json in ${folderPath}: ${err2.message}`);
|
|
18045
|
-
}
|
|
18046
|
-
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
18047
|
-
collections.push(...nestedCollections);
|
|
18048
|
-
}
|
|
18049
|
-
}
|
|
18050
|
-
return collections;
|
|
18051
|
-
} catch (err2) {
|
|
18052
|
-
if (err2.code === "ENOENT") {
|
|
18053
|
-
return [];
|
|
18054
|
-
}
|
|
18055
|
-
throw err2;
|
|
18056
|
-
}
|
|
18057
|
-
}
|
|
18058
|
-
async function loadCollectionMetaFiles2(contentRoot) {
|
|
18059
|
-
let root = null;
|
|
18060
|
-
const rootMetaPath = join3(contentRoot, ".collection.json");
|
|
18061
|
-
try {
|
|
18062
|
-
await fs29.access(rootMetaPath);
|
|
18063
|
-
} catch (err2) {
|
|
18064
|
-
if (err2.code === "ENOENT") {
|
|
18065
|
-
} else {
|
|
18066
|
-
throw err2;
|
|
18067
|
-
}
|
|
18068
|
-
}
|
|
18069
|
-
try {
|
|
18070
|
-
const content = await fs29.readFile(rootMetaPath, "utf-8");
|
|
18071
|
-
const parsed = JSON.parse(content);
|
|
18072
|
-
root = rootCollectionMetaSchema2.parse(parsed);
|
|
18073
|
-
} catch (err2) {
|
|
18074
|
-
const errno = err2.code;
|
|
18075
|
-
if (errno !== "ENOENT") {
|
|
18076
|
-
throw new Error(`Invalid root .collection.json`);
|
|
18077
|
-
}
|
|
18078
|
-
}
|
|
18079
|
-
const collections = await scanForCollectionMeta2(contentRoot);
|
|
18080
|
-
return { root, collections };
|
|
18081
|
-
}
|
|
18082
|
-
var entryTypeMetaSchema2, collectionMetaSchema2, rootCollectionMetaSchema2;
|
|
18083
|
-
var init_meta_loader = __esm({
|
|
18084
|
-
"dist/schema/meta-loader.js"() {
|
|
18085
|
-
"use strict";
|
|
18086
|
-
init_logger3();
|
|
18087
|
-
init_content_id_index();
|
|
18088
|
-
entryTypeMetaSchema2 = z16.object({
|
|
18089
|
-
name: z16.string().min(1),
|
|
18090
|
-
format: z16.enum(["md", "mdx", "json", "yaml"]),
|
|
18091
|
-
schema: z16.string().min(1),
|
|
18092
|
-
// Entry schema registry key (validated at resolution time)
|
|
18093
|
-
label: z16.string().optional(),
|
|
18094
|
-
description: z16.string().optional(),
|
|
18095
|
-
default: z16.boolean().optional(),
|
|
18096
|
-
maxItems: z16.number().int().positive().optional()
|
|
18097
|
-
});
|
|
18098
|
-
collectionMetaSchema2 = z16.object({
|
|
18099
|
-
name: z16.string().min(1),
|
|
18100
|
-
label: z16.string().optional(),
|
|
18101
|
-
description: z16.string().optional(),
|
|
18102
|
-
entries: z16.array(entryTypeMetaSchema2).optional(),
|
|
18103
|
-
order: z16.array(z16.string()).default([])
|
|
18104
|
-
// Embedded IDs for ordering; optional in the file, [] = alphabetical
|
|
18105
|
-
}).refine((data) => data.entries && data.entries.length > 0, {
|
|
18106
|
-
message: "Collection must have at least one entry type"
|
|
18107
|
-
});
|
|
18108
|
-
rootCollectionMetaSchema2 = z16.object({
|
|
18109
|
-
label: z16.string().optional(),
|
|
18110
|
-
entries: z16.array(entryTypeMetaSchema2).optional(),
|
|
18111
|
-
order: z16.array(z16.string()).optional()
|
|
18112
|
-
// Embedded IDs for ordering items
|
|
18113
|
-
});
|
|
18114
|
-
}
|
|
18115
|
-
});
|
|
18116
|
-
|
|
18117
|
-
// dist/branch-schema-cache.js
|
|
18118
|
-
import { LRUCache as LRUCache2 } from "lru-cache";
|
|
18119
|
-
var SNAPSHOT_MEMO_MAX_BYTES2, SCHEMA_GENERATION_RESOURCE2;
|
|
18120
|
-
var init_branch_schema_cache = __esm({
|
|
18121
|
-
"dist/branch-schema-cache.js"() {
|
|
18122
|
-
"use strict";
|
|
18123
|
-
SNAPSHOT_MEMO_MAX_BYTES2 = 64 * 1024 * 1024;
|
|
18124
|
-
SCHEMA_GENERATION_RESOURCE2 = "schema";
|
|
18346
|
+
return await fn();
|
|
18347
|
+
} finally {
|
|
18348
|
+
resolve2();
|
|
18349
|
+
if (locks2.get(key) === next)
|
|
18350
|
+
locks2.delete(key);
|
|
18125
18351
|
}
|
|
18126
|
-
});
|
|
18127
|
-
|
|
18128
|
-
// dist/content-index-generation.js
|
|
18129
|
-
async function bumpContentIndexGeneration2(root) {
|
|
18130
|
-
return bumpResourceGeneration2(root, RESOURCE3);
|
|
18131
|
-
}
|
|
18132
|
-
async function invalidateBranchContentCaches2(root) {
|
|
18133
|
-
await bumpContentIndexGeneration2(root);
|
|
18134
|
-
await bumpResourceGeneration2(root, SCHEMA_GENERATION_RESOURCE2);
|
|
18135
|
-
invalidateContentIndexesForRoot2(root);
|
|
18136
18352
|
}
|
|
18137
|
-
var
|
|
18138
|
-
var
|
|
18139
|
-
"dist/
|
|
18353
|
+
var locks2;
|
|
18354
|
+
var init_async_mutex = __esm({
|
|
18355
|
+
"dist/utils/async-mutex.js"() {
|
|
18140
18356
|
"use strict";
|
|
18141
|
-
|
|
18142
|
-
init_content_index_registry();
|
|
18143
|
-
init_branch_schema_cache();
|
|
18144
|
-
RESOURCE3 = "content-index";
|
|
18357
|
+
locks2 = /* @__PURE__ */ new Map();
|
|
18145
18358
|
}
|
|
18146
18359
|
});
|
|
18147
18360
|
|
|
18148
18361
|
// dist/utils/provisioning-lock.js
|
|
18149
|
-
import
|
|
18150
|
-
import
|
|
18362
|
+
import fs31 from "node:fs/promises";
|
|
18363
|
+
import path39 from "node:path";
|
|
18151
18364
|
import lockfile3 from "proper-lockfile";
|
|
18152
18365
|
function guardOnCompromised2(handler, lockPath) {
|
|
18153
18366
|
return (err2) => {
|
|
@@ -18193,14 +18406,14 @@ function releaseIgnoringAlreadyReleased2(release, lockPath) {
|
|
|
18193
18406
|
}
|
|
18194
18407
|
async function tryAcquireProvisioningLock2(lockTargetDir3, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS2, createParents = true) {
|
|
18195
18408
|
if (createParents) {
|
|
18196
|
-
await
|
|
18409
|
+
await fs31.mkdir(lockTargetDir3, { recursive: true });
|
|
18197
18410
|
} else {
|
|
18198
|
-
await
|
|
18411
|
+
await fs31.mkdir(lockTargetDir3).catch((err2) => {
|
|
18199
18412
|
if (!isNodeError(err2) || err2.code !== "EEXIST")
|
|
18200
18413
|
throw err2;
|
|
18201
18414
|
});
|
|
18202
18415
|
}
|
|
18203
|
-
const lockPath =
|
|
18416
|
+
const lockPath = path39.join(lockTargetDir3, lockName);
|
|
18204
18417
|
const release = await lockfile3.lock(lockPath, provisioningLockOptions2(lockPath, 0, onCompromised, staleMs));
|
|
18205
18418
|
return releaseIgnoringAlreadyReleased2(release, lockPath);
|
|
18206
18419
|
}
|
|
@@ -18214,1462 +18427,1556 @@ var init_provisioning_lock = __esm({
|
|
|
18214
18427
|
}
|
|
18215
18428
|
});
|
|
18216
18429
|
|
|
18217
|
-
// dist/utils/
|
|
18218
|
-
import
|
|
18219
|
-
import
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
|
|
18430
|
+
// dist/utils/occ-json-write.js
|
|
18431
|
+
import fs33 from "node:fs/promises";
|
|
18432
|
+
import path40 from "node:path";
|
|
18433
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
18434
|
+
import lockfile4 from "proper-lockfile";
|
|
18435
|
+
async function writeOccJsonFile2(filePath, payload, options) {
|
|
18436
|
+
const { expectedVersion, settleMs = 50, trailingNewline = false } = options;
|
|
18437
|
+
const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
|
|
18438
|
+
const writeId = randomUUID6();
|
|
18439
|
+
const data = { ...payload, version: newVersion, writeId };
|
|
18440
|
+
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
18441
|
+
const dir = path40.dirname(filePath);
|
|
18442
|
+
await fs33.mkdir(dir, { recursive: true });
|
|
18443
|
+
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
18444
|
+
await fs33.writeFile(tempPath, content, "utf-8");
|
|
18445
|
+
if (expectedVersion === null) {
|
|
18446
|
+
try {
|
|
18447
|
+
await fs33.link(tempPath, filePath);
|
|
18448
|
+
} catch (err2) {
|
|
18449
|
+
await fs33.unlink(tempPath).catch(() => {
|
|
18450
|
+
});
|
|
18451
|
+
if (isFileExistsError(err2)) {
|
|
18452
|
+
throw new OccWriteConflictError2();
|
|
18453
|
+
}
|
|
18454
|
+
throw err2;
|
|
18455
|
+
}
|
|
18456
|
+
await fs33.unlink(tempPath).catch(() => {
|
|
18457
|
+
});
|
|
18458
|
+
return { version: newVersion, writeId };
|
|
18459
|
+
}
|
|
18460
|
+
try {
|
|
18461
|
+
let currentVersion = null;
|
|
18462
|
+
try {
|
|
18463
|
+
const current = JSON.parse(await fs33.readFile(filePath, "utf-8"));
|
|
18464
|
+
currentVersion = current.version ?? 0;
|
|
18465
|
+
} catch {
|
|
18466
|
+
currentVersion = null;
|
|
18467
|
+
}
|
|
18468
|
+
if (currentVersion !== expectedVersion) {
|
|
18469
|
+
throw new OccWriteConflictError2();
|
|
18470
|
+
}
|
|
18471
|
+
await fs33.rename(tempPath, filePath);
|
|
18472
|
+
if (settleMs > 0) {
|
|
18473
|
+
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
18474
|
+
}
|
|
18475
|
+
const afterWrite = JSON.parse(await fs33.readFile(filePath, "utf-8"));
|
|
18476
|
+
if (afterWrite.writeId !== writeId) {
|
|
18477
|
+
throw new OccWriteConflictError2();
|
|
18478
|
+
}
|
|
18479
|
+
} catch (err2) {
|
|
18480
|
+
await fs33.unlink(tempPath).catch(() => {
|
|
18481
|
+
});
|
|
18482
|
+
throw err2;
|
|
18483
|
+
}
|
|
18484
|
+
return { version: newVersion, writeId };
|
|
18225
18485
|
}
|
|
18226
|
-
async function
|
|
18227
|
-
const
|
|
18228
|
-
|
|
18486
|
+
async function withOccRetry2(operation, options) {
|
|
18487
|
+
const maxAttempts = options?.maxAttempts ?? 10;
|
|
18488
|
+
const isRetryable = options?.isRetryable;
|
|
18489
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
18229
18490
|
try {
|
|
18230
|
-
return await
|
|
18491
|
+
return await operation();
|
|
18231
18492
|
} catch (err2) {
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18493
|
+
const retryable = err2 instanceof OccWriteConflictError2 || (isRetryable?.(err2) ?? false);
|
|
18494
|
+
if (retryable && attempt < maxAttempts) {
|
|
18495
|
+
const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
|
|
18496
|
+
const jitter = Math.random() * baseDelay;
|
|
18497
|
+
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
18498
|
+
continue;
|
|
18499
|
+
}
|
|
18500
|
+
throw err2;
|
|
18239
18501
|
}
|
|
18240
18502
|
}
|
|
18503
|
+
throw new Error("Unreachable");
|
|
18241
18504
|
}
|
|
18242
|
-
async function
|
|
18243
|
-
let compromised = false;
|
|
18244
|
-
const release = await acquireContentWriteLock2(branchRoot, waitMs, (err2) => {
|
|
18245
|
-
compromised = true;
|
|
18246
|
-
canopyLogWarn3(`[canopy] Content-write lock compromised mid-hold for ${branchRoot}:`, getErrorMessage(err2));
|
|
18247
|
-
});
|
|
18248
|
-
let result;
|
|
18505
|
+
async function withOccFileLock2(filePath, fn) {
|
|
18249
18506
|
try {
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18507
|
+
await fs33.mkdir(path40.dirname(filePath));
|
|
18508
|
+
} catch (err2) {
|
|
18509
|
+
if (!isNodeError(err2) || err2.code !== "EEXIST") {
|
|
18510
|
+
throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err2)}`);
|
|
18511
|
+
}
|
|
18255
18512
|
}
|
|
18256
|
-
|
|
18257
|
-
|
|
18513
|
+
const MAX_ATTEMPTS = 14;
|
|
18514
|
+
let release = null;
|
|
18515
|
+
for (let attempt = 1; release === null; attempt++) {
|
|
18516
|
+
try {
|
|
18517
|
+
release = await lockfile4.lock(filePath, {
|
|
18518
|
+
lockfilePath: `${filePath}.lock`,
|
|
18519
|
+
realpath: false,
|
|
18520
|
+
stale: 1e4,
|
|
18521
|
+
retries: 0,
|
|
18522
|
+
// A compromised lock (the lock dir vanished or refresh failed mid-hold, e.g. the branch
|
|
18523
|
+
// directory containing it was deleted, or an EFS waiter misread a cached mtime and took
|
|
18524
|
+
// it over) must not crash the process, which is proper-lockfile's default. Our critical
|
|
18525
|
+
// sections are short and idempotent-on-conflict (OCC verify inside); log and let the
|
|
18526
|
+
// section finish.
|
|
18527
|
+
onCompromised: guardOnCompromised2((err2) => canopyLogWarn3(`[canopy] OCC file lock compromised mid-hold for ${filePath}:`, getErrorMessage(err2)), filePath)
|
|
18528
|
+
});
|
|
18529
|
+
} catch (err2) {
|
|
18530
|
+
const code = isNodeError(err2) ? err2.code : void 0;
|
|
18531
|
+
const contended = code === "ELOCKED";
|
|
18532
|
+
if (!contended || attempt >= MAX_ATTEMPTS) {
|
|
18533
|
+
throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err2)}`);
|
|
18534
|
+
}
|
|
18535
|
+
const baseDelay = Math.min(50 * Math.pow(1.6, attempt - 1), 2e3);
|
|
18536
|
+
const jitter = Math.random() * baseDelay;
|
|
18537
|
+
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
18538
|
+
}
|
|
18539
|
+
}
|
|
18540
|
+
try {
|
|
18541
|
+
return await fn();
|
|
18542
|
+
} finally {
|
|
18543
|
+
await release().catch((err2) => {
|
|
18544
|
+
if (isNodeError(err2) && err2.code === "ENOENT")
|
|
18545
|
+
return;
|
|
18546
|
+
log13.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err2) });
|
|
18547
|
+
});
|
|
18258
18548
|
}
|
|
18259
|
-
return result;
|
|
18260
18549
|
}
|
|
18261
|
-
var
|
|
18262
|
-
var
|
|
18263
|
-
"dist/utils/
|
|
18550
|
+
var log13, OccWriteConflictError2;
|
|
18551
|
+
var init_occ_json_write = __esm({
|
|
18552
|
+
"dist/utils/occ-json-write.js"() {
|
|
18264
18553
|
"use strict";
|
|
18265
18554
|
init_error();
|
|
18555
|
+
init_debug2();
|
|
18266
18556
|
init_logger3();
|
|
18267
18557
|
init_provisioning_lock();
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
ContentWriteLockBusyError2 = class extends Error {
|
|
18272
|
-
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") {
|
|
18558
|
+
log13 = createDebugLogger3({ prefix: "OccJsonWrite" });
|
|
18559
|
+
OccWriteConflictError2 = class extends Error {
|
|
18560
|
+
constructor(message = "Concurrent modification detected") {
|
|
18273
18561
|
super(message);
|
|
18274
|
-
this.
|
|
18275
|
-
this.name = "ContentWriteLockBusyError";
|
|
18562
|
+
this.name = "OccWriteConflictError";
|
|
18276
18563
|
}
|
|
18277
18564
|
};
|
|
18278
|
-
sleep3 = (ms) => new Promise((resolve2) => {
|
|
18279
|
-
setTimeout(resolve2, ms);
|
|
18280
|
-
});
|
|
18281
|
-
CONTENT_WRITE_LOCK_STALE_MS2 = 3e4;
|
|
18282
|
-
heldRoots2 = new AsyncLocalStorage3();
|
|
18283
18565
|
}
|
|
18284
18566
|
});
|
|
18285
18567
|
|
|
18286
|
-
// dist/
|
|
18287
|
-
import
|
|
18288
|
-
import
|
|
18289
|
-
|
|
18290
|
-
|
|
18291
|
-
const normalizedResolved = path38.resolve(resolved);
|
|
18292
|
-
const normalizedParent = path38.resolve(parent);
|
|
18293
|
-
if (!normalizedResolved.startsWith(normalizedParent + path38.sep) && normalizedResolved !== normalizedParent) {
|
|
18294
|
-
throw new Error(`${label} escapes the expected directory: ${resolved}`);
|
|
18295
|
-
}
|
|
18568
|
+
// dist/branch-metadata.js
|
|
18569
|
+
import fs34 from "node:fs/promises";
|
|
18570
|
+
import path41 from "node:path";
|
|
18571
|
+
function isRecordedEditor2(branch, userId) {
|
|
18572
|
+
return (branch.editors?.includes(userId) ?? false) && (branch.uncommittedEditors?.includes(userId) ?? false);
|
|
18296
18573
|
}
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
const oldExists = await filePathExists(oldDir);
|
|
18300
|
-
if (oldExists) {
|
|
18301
|
-
await fs31.rename(oldDir, backupDir);
|
|
18302
|
-
}
|
|
18303
|
-
try {
|
|
18304
|
-
await fs31.rename(newDir, oldDir);
|
|
18305
|
-
} catch (err2) {
|
|
18306
|
-
if (oldExists) {
|
|
18307
|
-
await fs31.rename(backupDir, oldDir).catch(() => {
|
|
18308
|
-
});
|
|
18309
|
-
}
|
|
18310
|
-
throw err2;
|
|
18311
|
-
}
|
|
18312
|
-
if (oldExists) {
|
|
18313
|
-
await fs31.rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
18314
|
-
});
|
|
18315
|
-
}
|
|
18316
|
-
}
|
|
18317
|
-
async function listFilesRecursive(dir, prefix = "") {
|
|
18318
|
-
const results = [];
|
|
18319
|
-
let entries;
|
|
18320
|
-
try {
|
|
18321
|
-
entries = await fs31.readdir(dir, { withFileTypes: true });
|
|
18322
|
-
} catch {
|
|
18323
|
-
return results;
|
|
18324
|
-
}
|
|
18325
|
-
for (const entry of entries) {
|
|
18326
|
-
if (entry.name === ".git")
|
|
18327
|
-
continue;
|
|
18328
|
-
if (entry.isSymbolicLink())
|
|
18329
|
-
continue;
|
|
18330
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
18331
|
-
if (entry.isDirectory()) {
|
|
18332
|
-
results.push(...await listFilesRecursive(path38.join(dir, entry.name), rel));
|
|
18333
|
-
} else {
|
|
18334
|
-
results.push(rel);
|
|
18335
|
-
}
|
|
18336
|
-
}
|
|
18337
|
-
return results;
|
|
18338
|
-
}
|
|
18339
|
-
async function copyDir(src, dest) {
|
|
18340
|
-
await fs31.mkdir(dest, { recursive: true });
|
|
18341
|
-
const entries = await fs31.readdir(src, { withFileTypes: true });
|
|
18342
|
-
for (const entry of entries) {
|
|
18343
|
-
if (entry.name === ".git")
|
|
18344
|
-
continue;
|
|
18345
|
-
if (entry.isSymbolicLink())
|
|
18346
|
-
continue;
|
|
18347
|
-
const srcPath = path38.join(src, entry.name);
|
|
18348
|
-
const destPath = path38.join(dest, entry.name);
|
|
18349
|
-
if (entry.isDirectory()) {
|
|
18350
|
-
await copyDir(srcPath, destPath);
|
|
18351
|
-
} else {
|
|
18352
|
-
await fs31.copyFile(srcPath, destPath);
|
|
18353
|
-
}
|
|
18354
|
-
}
|
|
18355
|
-
}
|
|
18356
|
-
async function pushContentToWorkspace(options) {
|
|
18357
|
-
const { srcContentDir, branchPath, contentRoot } = options;
|
|
18358
|
-
if (!await filePathExists(srcContentDir)) {
|
|
18359
|
-
return { fileCount: 0, savedEditorState: false };
|
|
18360
|
-
}
|
|
18361
|
-
const wsContentDir = path38.join(branchPath, contentRoot);
|
|
18362
|
-
assertWithinDir(wsContentDir, branchPath, "content-root");
|
|
18363
|
-
return withContentWriteLock2(branchPath, () => replaceWorkspaceContent(options, wsContentDir));
|
|
18574
|
+
function withMember2(list, id) {
|
|
18575
|
+
return list?.includes(id) ? [...list] : [...list ?? [], id];
|
|
18364
18576
|
}
|
|
18365
|
-
|
|
18366
|
-
const
|
|
18367
|
-
|
|
18368
|
-
|
|
18369
|
-
|
|
18370
|
-
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18577
|
+
function mergeBranchMetadata2(existing, version, incoming, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
18578
|
+
const defaults = {
|
|
18579
|
+
name: "unknown",
|
|
18580
|
+
status: "editing",
|
|
18581
|
+
access: {},
|
|
18582
|
+
createdBy: "unknown",
|
|
18583
|
+
createdAt: now,
|
|
18584
|
+
updatedAt: now
|
|
18585
|
+
};
|
|
18586
|
+
return {
|
|
18587
|
+
// Envelope keys this code does not know survive, as unknown branch keys do below.
|
|
18588
|
+
...existing,
|
|
18589
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
18590
|
+
version: version ?? 0,
|
|
18591
|
+
branch: {
|
|
18592
|
+
...defaults,
|
|
18593
|
+
...existing?.branch,
|
|
18594
|
+
...incoming.branch,
|
|
18595
|
+
access: {
|
|
18596
|
+
...existing?.branch?.access,
|
|
18597
|
+
...incoming.branch?.access
|
|
18598
|
+
},
|
|
18599
|
+
// Immutable after creation
|
|
18600
|
+
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
18601
|
+
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
18602
|
+
// Fork point is recorded once at creation; later saves must not move it
|
|
18603
|
+
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
18604
|
+
// Always stamped fresh; the spreads above would otherwise let the creation-time value win
|
|
18605
|
+
// forever, freezing the timestamp the editor's Branches panel sorts and displays by
|
|
18606
|
+
updatedAt: now
|
|
18393
18607
|
}
|
|
18394
|
-
|
|
18395
|
-
if (baseTag)
|
|
18396
|
-
await wsGit.tag(["-f", baseTag]);
|
|
18397
|
-
return { fileCount: staged.length, savedEditorState };
|
|
18398
|
-
} finally {
|
|
18399
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
18400
|
-
}
|
|
18608
|
+
};
|
|
18401
18609
|
}
|
|
18402
|
-
var
|
|
18403
|
-
var
|
|
18404
|
-
"dist/
|
|
18610
|
+
var CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataFileManager2, loadBranchContext2;
|
|
18611
|
+
var init_branch_metadata = __esm({
|
|
18612
|
+
"dist/branch-metadata.js"() {
|
|
18405
18613
|
"use strict";
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18409
|
-
|
|
18410
|
-
|
|
18614
|
+
init_branch_registry();
|
|
18615
|
+
init_branch_metadata_file();
|
|
18616
|
+
init_paths();
|
|
18617
|
+
init_error();
|
|
18618
|
+
init_async_mutex();
|
|
18619
|
+
init_occ_json_write();
|
|
18620
|
+
CURRENT_SCHEMA_VERSION2 = 1;
|
|
18621
|
+
BranchMetadataConflictError2 = class extends Error {
|
|
18622
|
+
constructor(message = "Concurrent modification detected in branch metadata") {
|
|
18623
|
+
super(message);
|
|
18624
|
+
this.name = "BranchMetadataConflictError";
|
|
18625
|
+
}
|
|
18626
|
+
};
|
|
18627
|
+
BranchMetadataFileManager2 = class _BranchMetadataFileManager2 {
|
|
18628
|
+
constructor(branchRoot, baseRoot, options) {
|
|
18629
|
+
this.branchRoot = path41.resolve(branchRoot);
|
|
18630
|
+
this.filePath = path41.join(this.branchRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
|
|
18631
|
+
this.baseRoot = baseRoot;
|
|
18632
|
+
this.settleMs = options?.settleMs;
|
|
18633
|
+
}
|
|
18634
|
+
/** Read-only load, needing no baseRoot: registry scanning, loadBranchContext. */
|
|
18635
|
+
static async loadOnly(branchRoot) {
|
|
18636
|
+
return readBranchMetadataFile2(branchRoot);
|
|
18637
|
+
}
|
|
18638
|
+
/**
|
|
18639
|
+
* An instance wired for registry invalidation. API handlers use this, so the
|
|
18640
|
+
* registry cache is invalidated on update.
|
|
18641
|
+
*/
|
|
18642
|
+
static get(branchRoot, baseRoot, options) {
|
|
18643
|
+
return new _BranchMetadataFileManager2(branchRoot, baseRoot, options);
|
|
18644
|
+
}
|
|
18645
|
+
/**
|
|
18646
|
+
* The file the save cycle merges over. A corrupt one throws
|
|
18647
|
+
* {@link BranchMetadataCorruptError} rather than reading as absent, so a save
|
|
18648
|
+
* never lays defaults (status 'editing', no ACL) over a branch whose real
|
|
18649
|
+
* state is unknown; repair-metadata archives the file before it saves.
|
|
18650
|
+
*/
|
|
18651
|
+
async load() {
|
|
18652
|
+
const meta = await readBranchMetadataFile2(this.branchRoot);
|
|
18653
|
+
return { meta, version: meta?.version ?? null };
|
|
18654
|
+
}
|
|
18655
|
+
/**
|
|
18656
|
+
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
18657
|
+
* default here since payload shaping is branch-metadata's concern.
|
|
18658
|
+
*
|
|
18659
|
+
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
18660
|
+
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
18661
|
+
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
18662
|
+
* save() boundary would make withOccRetry's predicate miss it.
|
|
18663
|
+
*
|
|
18664
|
+
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
18665
|
+
* unlike comments.json.
|
|
18666
|
+
*/
|
|
18667
|
+
async write(meta, expectedVersion) {
|
|
18668
|
+
const payload = {
|
|
18669
|
+
...meta,
|
|
18670
|
+
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION2
|
|
18671
|
+
};
|
|
18672
|
+
return writeOccJsonFile2(this.filePath, payload, {
|
|
18673
|
+
expectedVersion,
|
|
18674
|
+
settleMs: this.settleMs,
|
|
18675
|
+
trailingNewline: true
|
|
18676
|
+
});
|
|
18677
|
+
}
|
|
18678
|
+
/**
|
|
18679
|
+
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
18680
|
+
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
18681
|
+
*
|
|
18682
|
+
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
18683
|
+
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
18684
|
+
* removes the branch directory, while the save reaches here after it, and
|
|
18685
|
+
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
18686
|
+
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
18687
|
+
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
18688
|
+
* save fails fast instead of paying for a lock.
|
|
18689
|
+
*
|
|
18690
|
+
* A save that passes the check just before a delete still cannot write: the
|
|
18691
|
+
* delete renames the tree away under the same lockfile, and that lockfile
|
|
18692
|
+
* creates only its own directory (`withOccFileLock`), so the save fails there.
|
|
18693
|
+
*/
|
|
18694
|
+
async save(incoming) {
|
|
18695
|
+
const saved = await this.saveIf(incoming, () => true);
|
|
18696
|
+
if (saved === null)
|
|
18697
|
+
throw new Error("unreachable: an unconditional save was skipped");
|
|
18698
|
+
return saved;
|
|
18699
|
+
}
|
|
18700
|
+
/**
|
|
18701
|
+
* {@link save}, applied only when `onlyIf` accepts the metadata on disk; resolves null when it
|
|
18702
|
+
* does not, having written nothing. `onlyIf` runs inside the lock stack against the version
|
|
18703
|
+
* the write is checked against, so the decision and the write are one compare-and-set.
|
|
18704
|
+
*/
|
|
18705
|
+
async saveIf(incoming, onlyIf) {
|
|
18706
|
+
return this.update((existing) => onlyIf(existing) ? incoming : null);
|
|
18707
|
+
}
|
|
18708
|
+
/**
|
|
18709
|
+
* A save whose update `compute` derives from the metadata on disk, inside the lock stack, so
|
|
18710
|
+
* an update built from the existing value (adding to a list) cannot lose a concurrent one.
|
|
18711
|
+
* `compute` returning null writes nothing and resolves null. `keepUpdatedAt` leaves
|
|
18712
|
+
* `updatedAt` as it was, for bookkeeping writes that are not a change to the branch.
|
|
18713
|
+
*/
|
|
18714
|
+
async update(compute, options = {}) {
|
|
18715
|
+
try {
|
|
18716
|
+
await fs34.stat(this.branchRoot);
|
|
18717
|
+
} catch (err2) {
|
|
18718
|
+
if (isNotFoundError(err2)) {
|
|
18719
|
+
throw new BranchMetadataConflictError2("Branch no longer exists");
|
|
18720
|
+
}
|
|
18721
|
+
throw err2;
|
|
18722
|
+
}
|
|
18723
|
+
let saved;
|
|
18724
|
+
try {
|
|
18725
|
+
saved = await withLock2(this.filePath, () => withOccFileLock2(this.filePath, () => withOccRetry2(async () => {
|
|
18726
|
+
const { meta: existing, version } = await this.load();
|
|
18727
|
+
const incoming = compute(existing);
|
|
18728
|
+
if (incoming === null)
|
|
18729
|
+
return null;
|
|
18730
|
+
const merged = mergeBranchMetadata2(existing, version, incoming);
|
|
18731
|
+
if (options.keepUpdatedAt && existing)
|
|
18732
|
+
merged.branch.updatedAt = existing.branch.updatedAt;
|
|
18733
|
+
const written = await this.write(merged, version);
|
|
18734
|
+
merged.version = written.version;
|
|
18735
|
+
merged.writeId = written.writeId;
|
|
18736
|
+
return merged;
|
|
18737
|
+
})));
|
|
18738
|
+
} catch (err2) {
|
|
18739
|
+
if (err2 instanceof OccWriteConflictError2) {
|
|
18740
|
+
throw new BranchMetadataConflictError2();
|
|
18741
|
+
}
|
|
18742
|
+
throw err2;
|
|
18743
|
+
}
|
|
18744
|
+
if (saved !== null)
|
|
18745
|
+
await this.invalidateRegistry();
|
|
18746
|
+
return saved;
|
|
18747
|
+
}
|
|
18748
|
+
/**
|
|
18749
|
+
* Adds `userId` to `editors` and `uncommittedEditors`. A lock-free read skips the write when
|
|
18750
|
+
* both already hold it, so only a user's first save since the last submit commit pays for the lock.
|
|
18751
|
+
* Never creates branch.json.
|
|
18752
|
+
*/
|
|
18753
|
+
async recordEditor(userId) {
|
|
18754
|
+
const current = await readBranchMetadataFile2(this.branchRoot);
|
|
18755
|
+
if (!current || isRecordedEditor2(current.branch, userId))
|
|
18756
|
+
return;
|
|
18757
|
+
await this.update((existing) => existing && !isRecordedEditor2(existing.branch, userId) ? {
|
|
18758
|
+
branch: {
|
|
18759
|
+
editors: withMember2(existing.branch.editors, userId),
|
|
18760
|
+
uncommittedEditors: withMember2(existing.branch.uncommittedEditors, userId)
|
|
18761
|
+
}
|
|
18762
|
+
} : null, { keepUpdatedAt: true });
|
|
18763
|
+
}
|
|
18764
|
+
/**
|
|
18765
|
+
* Removes `userIds` from `uncommittedEditors` once a submit commit's trailers name them. Ids
|
|
18766
|
+
* recorded after the commit read them stay, so the next commit names them.
|
|
18767
|
+
*/
|
|
18768
|
+
async markEditorsCommitted(userIds) {
|
|
18769
|
+
const committed = new Set(userIds);
|
|
18770
|
+
if (committed.size === 0)
|
|
18771
|
+
return;
|
|
18772
|
+
await this.update((existing) => {
|
|
18773
|
+
const pending = existing?.branch.uncommittedEditors ?? [];
|
|
18774
|
+
const remaining = pending.filter((id) => !committed.has(id));
|
|
18775
|
+
if (remaining.length === pending.length)
|
|
18776
|
+
return null;
|
|
18777
|
+
return { branch: { uncommittedEditors: remaining.length > 0 ? remaining : void 0 } };
|
|
18778
|
+
}, { keepUpdatedAt: true });
|
|
18779
|
+
}
|
|
18780
|
+
/** Invalidate the registry cache so the next list() regenerates. */
|
|
18781
|
+
async invalidateRegistry() {
|
|
18782
|
+
const registry3 = new BranchRegistry2(this.baseRoot);
|
|
18783
|
+
await registry3.invalidate();
|
|
18784
|
+
}
|
|
18785
|
+
};
|
|
18786
|
+
loadBranchContext2 = async (options) => {
|
|
18787
|
+
const { branchRoot, baseRoot } = resolveBranchPath2({
|
|
18788
|
+
branchName: options.branchName,
|
|
18789
|
+
mode: options.mode,
|
|
18790
|
+
basePathOverride: options.basePathOverride
|
|
18791
|
+
});
|
|
18792
|
+
const meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
|
|
18793
|
+
if (!meta) {
|
|
18794
|
+
return null;
|
|
18795
|
+
}
|
|
18796
|
+
return {
|
|
18797
|
+
branch: meta.branch,
|
|
18798
|
+
branchRoot,
|
|
18799
|
+
baseRoot
|
|
18800
|
+
};
|
|
18801
|
+
};
|
|
18411
18802
|
}
|
|
18412
18803
|
});
|
|
18413
18804
|
|
|
18414
|
-
// dist/
|
|
18415
|
-
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
await fs33.mkdir(path39.dirname(excludePath), { recursive: true });
|
|
18421
|
-
let content = "";
|
|
18422
|
-
try {
|
|
18423
|
-
content = await fs33.readFile(excludePath, "utf-8");
|
|
18424
|
-
} catch (err2) {
|
|
18425
|
-
if (!isNotFoundError(err2))
|
|
18426
|
-
throw err2;
|
|
18427
|
-
}
|
|
18428
|
-
const lines = content.split("\n");
|
|
18429
|
-
if (lines.some((line) => line.trim() === pattern)) {
|
|
18430
|
-
log12.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
18431
|
-
return;
|
|
18805
|
+
// dist/utils/base-branch.js
|
|
18806
|
+
function baseBranchOf2(config) {
|
|
18807
|
+
if (config.defaultBaseBranch)
|
|
18808
|
+
return config.defaultBaseBranch;
|
|
18809
|
+
if (config.mode === "prod") {
|
|
18810
|
+
throw new BaseBranchUnresolvedError2("the prod base branch has not been resolved.");
|
|
18432
18811
|
}
|
|
18433
|
-
|
|
18434
|
-
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
18435
|
-
await fs33.writeFile(excludePath, newContent, "utf-8");
|
|
18436
|
-
log12.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
18812
|
+
return "main";
|
|
18437
18813
|
}
|
|
18438
|
-
var
|
|
18439
|
-
var
|
|
18440
|
-
"dist/
|
|
18814
|
+
var BaseBranchUnresolvedError2;
|
|
18815
|
+
var init_base_branch = __esm({
|
|
18816
|
+
"dist/utils/base-branch.js"() {
|
|
18441
18817
|
"use strict";
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18818
|
+
BaseBranchUnresolvedError2 = class extends Error {
|
|
18819
|
+
constructor(detail) {
|
|
18820
|
+
super(`CanopyCMS: ${detail} Set defaultBaseBranch in your CanopyCMS config to the branch editing branches fork from.`);
|
|
18821
|
+
this.name = "BaseBranchUnresolvedError";
|
|
18822
|
+
}
|
|
18823
|
+
};
|
|
18445
18824
|
}
|
|
18446
18825
|
});
|
|
18447
18826
|
|
|
18448
|
-
// dist/
|
|
18449
|
-
|
|
18450
|
-
__export(sync_exports, {
|
|
18451
|
-
sync: () => sync
|
|
18452
|
-
});
|
|
18453
|
-
import fs34 from "node:fs/promises";
|
|
18454
|
-
import path40 from "node:path";
|
|
18455
|
-
import { simpleGit as simpleGit7 } from "simple-git";
|
|
18456
|
-
import * as p2 from "@clack/prompts";
|
|
18457
|
-
function lockBusyAsSyncError(err2, branchName) {
|
|
18458
|
-
if (!(err2 instanceof ContentWriteLockBusyError2))
|
|
18459
|
-
return err2;
|
|
18460
|
-
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.`);
|
|
18461
|
-
}
|
|
18462
|
-
async function withWorkspaceLock(branchPath, branchName, fn) {
|
|
18827
|
+
// dist/branch-param.js
|
|
18828
|
+
function cmsBranchNamespace2(config) {
|
|
18463
18829
|
try {
|
|
18464
|
-
return
|
|
18465
|
-
} catch
|
|
18466
|
-
|
|
18830
|
+
return operatingStrategy3(config.mode).getBranchNamespace(config);
|
|
18831
|
+
} catch {
|
|
18832
|
+
return null;
|
|
18467
18833
|
}
|
|
18468
18834
|
}
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18835
|
+
function namesBaseOrActive2(param, config) {
|
|
18836
|
+
let base;
|
|
18837
|
+
try {
|
|
18838
|
+
base = baseBranchOf2(config);
|
|
18839
|
+
} catch (err2) {
|
|
18840
|
+
if (err2 instanceof BaseBranchUnresolvedError2)
|
|
18473
18841
|
return false;
|
|
18474
|
-
|
|
18475
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
18476
|
-
return true;
|
|
18477
|
-
});
|
|
18478
|
-
}
|
|
18479
|
-
async function detectCurrentBranch(projectDir) {
|
|
18480
|
-
const git = simpleGit7({ baseDir: projectDir });
|
|
18481
|
-
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
18482
|
-
if (branch === "HEAD")
|
|
18483
|
-
throw new Error("Detached HEAD \u2014 use --branch to specify a branch name");
|
|
18484
|
-
return branch;
|
|
18485
|
-
}
|
|
18486
|
-
async function selectBranch(options, branchesDir) {
|
|
18487
|
-
let branches = [];
|
|
18488
|
-
if (await filePathExists(branchesDir)) {
|
|
18489
|
-
const entries = await fs34.readdir(branchesDir, { withFileTypes: true });
|
|
18490
|
-
branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
18491
|
-
}
|
|
18492
|
-
let branchName = options.branch;
|
|
18493
|
-
if (!branchName) {
|
|
18494
|
-
try {
|
|
18495
|
-
branchName = await detectCurrentBranch(options.projectDir);
|
|
18496
|
-
} catch {
|
|
18497
|
-
}
|
|
18842
|
+
throw err2;
|
|
18498
18843
|
}
|
|
18499
|
-
|
|
18500
|
-
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18844
|
+
return param === base || param === (config.defaultActiveBranch ?? base);
|
|
18845
|
+
}
|
|
18846
|
+
async function resolveBranchParam(param, config, options = {}) {
|
|
18847
|
+
const load = (branchName) => loadBranchContext2({ branchName, mode: config.mode, basePathOverride: options.basePathOverride });
|
|
18848
|
+
if (isExactBranchForm(param)) {
|
|
18849
|
+
const exact = exactBranchName(param);
|
|
18850
|
+
if (exact === null)
|
|
18851
|
+
return null;
|
|
18852
|
+
const named = await load(exact);
|
|
18853
|
+
return named?.branch.name === exact ? named : null;
|
|
18854
|
+
}
|
|
18855
|
+
if (namesBaseOrActive2(param, config))
|
|
18856
|
+
return load(param);
|
|
18857
|
+
const namespace = cmsBranchNamespace2(config);
|
|
18858
|
+
if (namespace !== null && !param.includes("/")) {
|
|
18859
|
+
const ref = `${namespace}${param}`;
|
|
18860
|
+
const inNamespace = await load(ref).catch((err2) => {
|
|
18861
|
+
if (namesNoWorkspace(err2))
|
|
18512
18862
|
return null;
|
|
18513
|
-
|
|
18514
|
-
|
|
18515
|
-
|
|
18516
|
-
|
|
18517
|
-
const branchPath = path40.join(branchesDir, branchName);
|
|
18518
|
-
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
18519
|
-
if (!branches.includes(branchName)) {
|
|
18520
|
-
if (options.autoCreate) {
|
|
18521
|
-
await fs34.mkdir(branchPath, { recursive: true });
|
|
18522
|
-
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
18523
|
-
await wsGit.init();
|
|
18524
|
-
await wsGit.checkoutLocalBranch(branchName);
|
|
18525
|
-
await wsGit.raw(["commit", "--allow-empty", "-m", "init: workspace created by sync"]);
|
|
18526
|
-
await ensureGitExcludePattern2(branchPath, operatingStrategy3("dev").getGitExcludePattern());
|
|
18527
|
-
p2.log.info(`Created branch workspace: ${branchName}`);
|
|
18528
|
-
} else {
|
|
18529
|
-
const available = branches.length > 0 ? ` Available branches: ${branches.join(", ")}` : "";
|
|
18530
|
-
throw new SyncError(`Branch workspace "${branchName}" not found.${available}`);
|
|
18531
|
-
}
|
|
18863
|
+
throw err2;
|
|
18864
|
+
});
|
|
18865
|
+
if (inNamespace?.branch.name === ref)
|
|
18866
|
+
return inNamespace;
|
|
18532
18867
|
}
|
|
18533
|
-
|
|
18868
|
+
const outside = await load(param);
|
|
18869
|
+
return outside?.branch.name === param ? outside : null;
|
|
18534
18870
|
}
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
}
|
|
18544
|
-
const branchName = await selectBranch({ ...options, projectDir, autoCreate: true }, branchesDir);
|
|
18545
|
-
if (!branchName)
|
|
18546
|
-
return { fileCount: 0 };
|
|
18547
|
-
const branchPath = path40.join(branchesDir, branchName);
|
|
18548
|
-
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
18549
|
-
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
18550
|
-
const status = await wsGit.status();
|
|
18551
|
-
if (status.conflicted.length > 0) {
|
|
18552
|
-
p2.log.error("Branch workspace has unresolved merge conflicts.");
|
|
18553
|
-
if (!options.force) {
|
|
18554
|
-
const shouldAbort = await p2.confirm({
|
|
18555
|
-
message: "Abort the merge and restore workspace to pre-merge state?",
|
|
18556
|
-
initialValue: false
|
|
18557
|
-
});
|
|
18558
|
-
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
18559
|
-
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
18560
|
-
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
18561
|
-
}
|
|
18562
|
-
}
|
|
18563
|
-
}
|
|
18564
|
-
return { fileCount: 0 };
|
|
18565
|
-
}
|
|
18566
|
-
const editorChanges = status.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
18567
|
-
if (editorChanges.length > 0 && !options.force) {
|
|
18568
|
-
p2.log.warn(`Branch workspace has ${editorChanges.length} uncommitted change(s) that will be committed to history then overwritten:`);
|
|
18569
|
-
for (const file of editorChanges.slice(0, 10)) {
|
|
18570
|
-
p2.log.warn(` ${file.path}`);
|
|
18571
|
-
}
|
|
18572
|
-
if (editorChanges.length > 10) {
|
|
18573
|
-
p2.log.warn(` ... and ${editorChanges.length - 10} more`);
|
|
18574
|
-
}
|
|
18575
|
-
const confirm5 = await p2.confirm({
|
|
18576
|
-
message: "Continue? Editor changes will be preserved in git history.",
|
|
18577
|
-
initialValue: false
|
|
18578
|
-
});
|
|
18579
|
-
if (p2.isCancel(confirm5) || !confirm5) {
|
|
18580
|
-
p2.cancel("Push cancelled.");
|
|
18581
|
-
return { fileCount: 0 };
|
|
18582
|
-
}
|
|
18871
|
+
var init_branch_param = __esm({
|
|
18872
|
+
"dist/branch-param.js"() {
|
|
18873
|
+
"use strict";
|
|
18874
|
+
init_branch_metadata();
|
|
18875
|
+
init_operating_mode2();
|
|
18876
|
+
init_branch();
|
|
18877
|
+
init_validation3();
|
|
18878
|
+
init_base_branch();
|
|
18583
18879
|
}
|
|
18584
|
-
|
|
18585
|
-
|
|
18880
|
+
});
|
|
18881
|
+
|
|
18882
|
+
// dist/utils/git.js
|
|
18883
|
+
import { simpleGit as simpleGit4 } from "simple-git";
|
|
18884
|
+
function isCanopyInternalPath(repoRelativePath) {
|
|
18885
|
+
return repoRelativePath === CANOPY_META_DIR2 || repoRelativePath.startsWith(`${CANOPY_META_DIR2}/`);
|
|
18886
|
+
}
|
|
18887
|
+
async function stageAllExceptCanopyState2(git) {
|
|
18888
|
+
await git.raw(["add", "-A", "--sparse"]);
|
|
18889
|
+
await git.raw(["reset", "-q", "--", CANOPY_META_DIR2]);
|
|
18890
|
+
}
|
|
18891
|
+
async function detectHeadBranch2(repoRoot, fallback = "main") {
|
|
18586
18892
|
try {
|
|
18587
|
-
const
|
|
18588
|
-
|
|
18589
|
-
|
|
18590
|
-
|
|
18591
|
-
|
|
18592
|
-
saveEditorStateMessage: "sync: save editor state before push"
|
|
18593
|
-
});
|
|
18594
|
-
fileCount = result.fileCount;
|
|
18595
|
-
if (result.savedEditorState)
|
|
18596
|
-
p2.log.info("Committed editor changes to history before push");
|
|
18597
|
-
} catch (err2) {
|
|
18598
|
-
throw lockBusyAsSyncError(err2, branchName);
|
|
18599
|
-
}
|
|
18600
|
-
if (fileCount === 0) {
|
|
18601
|
-
p2.log.info("Content is already up to date \u2014 nothing to push");
|
|
18602
|
-
} else {
|
|
18603
|
-
p2.log.success(`Pushed ${fileCount} file change(s) to branch "${branchName}"`);
|
|
18893
|
+
const git = simpleGit4({ baseDir: repoRoot });
|
|
18894
|
+
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
18895
|
+
return head && head !== "HEAD" ? head : fallback;
|
|
18896
|
+
} catch {
|
|
18897
|
+
return fallback;
|
|
18604
18898
|
}
|
|
18605
|
-
return { fileCount };
|
|
18606
18899
|
}
|
|
18607
|
-
|
|
18608
|
-
|
|
18609
|
-
|
|
18610
|
-
|
|
18611
|
-
|
|
18612
|
-
if (!branchName)
|
|
18613
|
-
return { fileCount: 0 };
|
|
18614
|
-
const branchPath = path40.join(branchesDir, branchName);
|
|
18615
|
-
assertWithinDir(branchPath, branchesDir, "--branch");
|
|
18616
|
-
const branchContentDir = path40.join(branchPath, contentRoot);
|
|
18617
|
-
assertWithinDir(branchContentDir, branchPath, "--content-root");
|
|
18618
|
-
if (!await filePathExists(branchContentDir)) {
|
|
18619
|
-
throw new SyncError(`Content directory not found in branch workspace: ${branchName}/${contentRoot}`);
|
|
18620
|
-
}
|
|
18621
|
-
p2.log.step(`Pulling content from branch: ${branchName}`);
|
|
18622
|
-
const destContentDir = path40.join(projectDir, contentRoot);
|
|
18623
|
-
assertWithinDir(destContentDir, projectDir, "--content-root");
|
|
18624
|
-
const sourceGit = simpleGit7({ baseDir: projectDir });
|
|
18625
|
-
const status = await sourceGit.status();
|
|
18626
|
-
const uncommittedContent = status.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
18627
|
-
const untrackedLosses = [];
|
|
18628
|
-
if (await filePathExists(destContentDir)) {
|
|
18629
|
-
const localFiles = await listFilesRecursive(destContentDir);
|
|
18630
|
-
const branchFiles = new Set(await listFilesRecursive(branchContentDir));
|
|
18631
|
-
for (const file of localFiles) {
|
|
18632
|
-
if (!branchFiles.has(file)) {
|
|
18633
|
-
const relativePath = contentRoot + "/" + file;
|
|
18634
|
-
if (!uncommittedContent.some((f) => f.path === relativePath)) {
|
|
18635
|
-
untrackedLosses.push(relativePath);
|
|
18636
|
-
}
|
|
18637
|
-
}
|
|
18638
|
-
}
|
|
18900
|
+
var CANOPY_META_DIR2;
|
|
18901
|
+
var init_git = __esm({
|
|
18902
|
+
"dist/utils/git.js"() {
|
|
18903
|
+
"use strict";
|
|
18904
|
+
CANOPY_META_DIR2 = ".canopy-meta";
|
|
18639
18905
|
}
|
|
18640
|
-
|
|
18641
|
-
|
|
18642
|
-
|
|
18643
|
-
|
|
18644
|
-
|
|
18645
|
-
|
|
18646
|
-
|
|
18647
|
-
|
|
18648
|
-
|
|
18649
|
-
|
|
18650
|
-
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
if (untrackedLosses.length > 10) {
|
|
18657
|
-
p2.log.warn(` ... and ${untrackedLosses.length - 10} more`);
|
|
18906
|
+
});
|
|
18907
|
+
|
|
18908
|
+
// dist/content-index-registry.js
|
|
18909
|
+
import path43 from "node:path";
|
|
18910
|
+
function invalidateContentIndexesForRoot2(root) {
|
|
18911
|
+
const rootKey = path43.resolve(root);
|
|
18912
|
+
const prefix = rootKey + path43.sep;
|
|
18913
|
+
for (const [storeRoot, refs] of registry2) {
|
|
18914
|
+
if (storeRoot !== rootKey && !storeRoot.startsWith(prefix))
|
|
18915
|
+
continue;
|
|
18916
|
+
for (const ref of refs) {
|
|
18917
|
+
const target = ref.deref();
|
|
18918
|
+
if (target) {
|
|
18919
|
+
target.invalidateIndex();
|
|
18920
|
+
} else {
|
|
18921
|
+
refs.delete(ref);
|
|
18658
18922
|
}
|
|
18659
18923
|
}
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
initialValue: false
|
|
18663
|
-
});
|
|
18664
|
-
if (p2.isCancel(confirm5) || !confirm5) {
|
|
18665
|
-
p2.cancel("Pull cancelled. Commit or stash your changes first.");
|
|
18666
|
-
return { fileCount: 0 };
|
|
18667
|
-
}
|
|
18924
|
+
if (refs.size === 0)
|
|
18925
|
+
registry2.delete(storeRoot);
|
|
18668
18926
|
}
|
|
18669
|
-
|
|
18670
|
-
|
|
18671
|
-
|
|
18672
|
-
|
|
18673
|
-
|
|
18674
|
-
|
|
18927
|
+
}
|
|
18928
|
+
var registry2, finalization2;
|
|
18929
|
+
var init_content_index_registry = __esm({
|
|
18930
|
+
"dist/content-index-registry.js"() {
|
|
18931
|
+
"use strict";
|
|
18932
|
+
registry2 = /* @__PURE__ */ new Map();
|
|
18933
|
+
finalization2 = new FinalizationRegistry(({ rootKey, ref }) => {
|
|
18934
|
+
const refs = registry2.get(rootKey);
|
|
18935
|
+
if (!refs)
|
|
18936
|
+
return;
|
|
18937
|
+
refs.delete(ref);
|
|
18938
|
+
if (refs.size === 0)
|
|
18939
|
+
registry2.delete(rootKey);
|
|
18675
18940
|
});
|
|
18676
|
-
throw err2;
|
|
18677
|
-
}
|
|
18678
|
-
const postStatus = await sourceGit.status();
|
|
18679
|
-
const contentChanges = postStatus.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
18680
|
-
if (contentChanges.length === 0) {
|
|
18681
|
-
p2.log.info("Content is already up to date \u2014 nothing to pull");
|
|
18682
|
-
} else {
|
|
18683
|
-
p2.log.success(`Pulled ${contentChanges.length} changed file(s) from branch "${branchName}"`);
|
|
18684
|
-
for (const file of contentChanges.slice(0, 20)) {
|
|
18685
|
-
const indicator = file.working_dir === "?" ? "A" : file.working_dir || file.index;
|
|
18686
|
-
p2.log.info(` ${indicator} ${file.path}`);
|
|
18687
|
-
}
|
|
18688
|
-
if (contentChanges.length > 20) {
|
|
18689
|
-
p2.log.info(` ... and ${contentChanges.length - 20} more`);
|
|
18690
|
-
}
|
|
18691
|
-
p2.log.info("\nReview the changes, then git add and commit when ready.");
|
|
18692
18941
|
}
|
|
18693
|
-
|
|
18942
|
+
});
|
|
18943
|
+
|
|
18944
|
+
// dist/id.js
|
|
18945
|
+
import { generate as generate2 } from "short-uuid";
|
|
18946
|
+
function generateId2() {
|
|
18947
|
+
const full = generate2();
|
|
18948
|
+
return full.substring(0, 12);
|
|
18694
18949
|
}
|
|
18695
|
-
|
|
18696
|
-
|
|
18697
|
-
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
|
|
18701
|
-
if (!await filePathExists(srcContentDir)) {
|
|
18702
|
-
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
18950
|
+
var isValidId2;
|
|
18951
|
+
var init_id = __esm({
|
|
18952
|
+
"dist/id.js"() {
|
|
18953
|
+
"use strict";
|
|
18954
|
+
init_validation3();
|
|
18955
|
+
isValidId2 = isValidContentId2;
|
|
18703
18956
|
}
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
|
|
18709
|
-
|
|
18710
|
-
|
|
18711
|
-
|
|
18712
|
-
|
|
18713
|
-
|
|
18714
|
-
|
|
18715
|
-
|
|
18716
|
-
|
|
18717
|
-
|
|
18718
|
-
|
|
18719
|
-
|
|
18720
|
-
|
|
18721
|
-
|
|
18957
|
+
});
|
|
18958
|
+
|
|
18959
|
+
// dist/utils/format.js
|
|
18960
|
+
var getFormatExtension2;
|
|
18961
|
+
var init_format = __esm({
|
|
18962
|
+
"dist/utils/format.js"() {
|
|
18963
|
+
"use strict";
|
|
18964
|
+
getFormatExtension2 = (format) => {
|
|
18965
|
+
if (format === "md")
|
|
18966
|
+
return ".md";
|
|
18967
|
+
if (format === "mdx")
|
|
18968
|
+
return ".mdx";
|
|
18969
|
+
if (format === "yaml")
|
|
18970
|
+
return ".yaml";
|
|
18971
|
+
return ".json";
|
|
18972
|
+
};
|
|
18973
|
+
}
|
|
18974
|
+
});
|
|
18975
|
+
|
|
18976
|
+
// dist/content-id-index.js
|
|
18977
|
+
function extractIdFromFilename2(filename) {
|
|
18978
|
+
if (filename.startsWith(".")) {
|
|
18979
|
+
return null;
|
|
18980
|
+
}
|
|
18981
|
+
const parts = filename.split(".");
|
|
18982
|
+
if (parts.length >= 3) {
|
|
18983
|
+
const candidate = parts[parts.length - 2];
|
|
18984
|
+
if (isValidId2(candidate))
|
|
18985
|
+
return candidate;
|
|
18986
|
+
}
|
|
18987
|
+
if (parts.length === 2) {
|
|
18988
|
+
const candidate = parts[parts.length - 1];
|
|
18989
|
+
if (isValidId2(candidate))
|
|
18990
|
+
return candidate;
|
|
18991
|
+
}
|
|
18992
|
+
return null;
|
|
18993
|
+
}
|
|
18994
|
+
function extractSlugFromFilename2(filename, entryTypeName) {
|
|
18995
|
+
const parts = filename.split(".");
|
|
18996
|
+
if (parts.length >= 3) {
|
|
18997
|
+
const possibleId = parts[parts.length - 2];
|
|
18998
|
+
if (isValidId2(possibleId)) {
|
|
18999
|
+
let slugParts = parts.slice(0, parts.length - 2);
|
|
19000
|
+
if (entryTypeName && slugParts.length > 1 && slugParts[0] === entryTypeName) {
|
|
19001
|
+
slugParts = slugParts.slice(1);
|
|
19002
|
+
} else if (parts.length >= 4 && slugParts.length > 1) {
|
|
19003
|
+
slugParts = slugParts.slice(1);
|
|
18722
19004
|
}
|
|
19005
|
+
return slugParts.join(".").toLowerCase();
|
|
18723
19006
|
}
|
|
18724
|
-
return { pushed: 0, pulled: 0 };
|
|
18725
19007
|
}
|
|
18726
|
-
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
const status = await wsGit.status();
|
|
18732
|
-
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
18733
|
-
await stageAllExceptCanopyState2(wsGit);
|
|
18734
|
-
await wsGit.commit("sync: save editor state before merge");
|
|
18735
|
-
p2.log.info("Committed editor changes before merge");
|
|
19008
|
+
if (parts.length === 2) {
|
|
19009
|
+
const possibleId = parts[parts.length - 1];
|
|
19010
|
+
if (isValidId2(possibleId)) {
|
|
19011
|
+
return parts[0].toLowerCase();
|
|
19012
|
+
}
|
|
18736
19013
|
}
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
await wsGit.raw(["rev-parse", SYNC_BASE_TAG]);
|
|
18740
|
-
baseRef = SYNC_BASE_TAG;
|
|
18741
|
-
} catch {
|
|
18742
|
-
baseRef = "HEAD";
|
|
19014
|
+
if (parts.length > 1) {
|
|
19015
|
+
return parts.slice(0, -1).join(".").toLowerCase();
|
|
18743
19016
|
}
|
|
18744
|
-
|
|
19017
|
+
return filename.toLowerCase();
|
|
19018
|
+
}
|
|
19019
|
+
var ENTRY_EXTENSIONS2;
|
|
19020
|
+
var init_content_id_index = __esm({
|
|
19021
|
+
"dist/content-id-index.js"() {
|
|
19022
|
+
"use strict";
|
|
19023
|
+
init_id();
|
|
19024
|
+
init_format();
|
|
19025
|
+
ENTRY_EXTENSIONS2 = new Set(Object.keys({ md: true, mdx: true, json: true, yaml: true }).map((format) => getFormatExtension2(format)));
|
|
19026
|
+
}
|
|
19027
|
+
});
|
|
19028
|
+
|
|
19029
|
+
// dist/schema/meta-loader.js
|
|
19030
|
+
import { promises as fs35 } from "fs";
|
|
19031
|
+
import { join as join3 } from "pathe";
|
|
19032
|
+
import { z as z18 } from "zod";
|
|
19033
|
+
import chokidar2 from "chokidar";
|
|
19034
|
+
function stripEmbeddedIdFromName2(name) {
|
|
19035
|
+
return extractSlugFromFilename2(name);
|
|
19036
|
+
}
|
|
19037
|
+
async function scanForCollectionMeta2(baseDir, relativePath = "") {
|
|
19038
|
+
const collections = [];
|
|
18745
19039
|
try {
|
|
18746
|
-
const
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
});
|
|
18757
|
-
await wsGit.checkout(currentBranch);
|
|
18758
|
-
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
18759
|
-
});
|
|
18760
|
-
throw err2;
|
|
18761
|
-
}
|
|
18762
|
-
await stageAllExceptCanopyState2(wsGit);
|
|
18763
|
-
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
18764
|
-
if (incomingFiles.length === 0) {
|
|
18765
|
-
await wsGit.checkout(currentBranch);
|
|
18766
|
-
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
18767
|
-
p2.log.info("No working-tree changes to merge \u2014 pulling editor changes only");
|
|
18768
|
-
const pullResult2 = await syncPull({ ...options, branch: branchName, force: true });
|
|
18769
|
-
return { pushed: 0, pulled: pullResult2.fileCount };
|
|
18770
|
-
}
|
|
18771
|
-
await wsGit.commit("sync: incoming working-tree changes");
|
|
18772
|
-
await wsGit.checkout(currentBranch);
|
|
18773
|
-
p2.log.step("Merging working-tree changes with editor changes...");
|
|
18774
|
-
try {
|
|
18775
|
-
await wsGit.merge([incomingBranch, "--no-edit"]);
|
|
18776
|
-
} catch (mergeError) {
|
|
18777
|
-
let mergeStatus;
|
|
19040
|
+
const entries = await fs35.readdir(baseDir, { withFileTypes: true });
|
|
19041
|
+
for (const entry of entries) {
|
|
19042
|
+
if (!entry.isDirectory())
|
|
19043
|
+
continue;
|
|
19044
|
+
const folderName = entry.name;
|
|
19045
|
+
const logicalName = stripEmbeddedIdFromName2(folderName);
|
|
19046
|
+
const collectionContentId = extractIdFromFilename2(folderName) ?? void 0;
|
|
19047
|
+
const folderPath = relativePath ? `${relativePath}/${logicalName}` : logicalName;
|
|
19048
|
+
const absolutePath = join3(baseDir, folderName);
|
|
19049
|
+
const metaPath = join3(absolutePath, ".collection.json");
|
|
18778
19050
|
try {
|
|
18779
|
-
|
|
18780
|
-
|
|
18781
|
-
|
|
19051
|
+
await fs35.access(metaPath);
|
|
19052
|
+
const content = await fs35.readFile(metaPath, "utf-8");
|
|
19053
|
+
const parsed = JSON.parse(content);
|
|
19054
|
+
const meta = collectionMetaSchema2.parse(parsed);
|
|
19055
|
+
collections.push({
|
|
19056
|
+
...meta,
|
|
19057
|
+
path: folderPath,
|
|
19058
|
+
contentId: collectionContentId
|
|
18782
19059
|
});
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
19060
|
+
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
19061
|
+
collections.push(...nestedCollections);
|
|
19062
|
+
} catch (err2) {
|
|
19063
|
+
if (err2.code !== "ENOENT") {
|
|
19064
|
+
canopyLogError2(`Error loading ${metaPath}:`, err2);
|
|
19065
|
+
throw new Error(`Invalid .collection.json in ${folderPath}: ${err2.message}`);
|
|
18789
19066
|
}
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
p2.log.info("Or abort the merge with: canopycms sync abort");
|
|
18793
|
-
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
18794
|
-
});
|
|
18795
|
-
return { pushed: 0, pulled: 0 };
|
|
19067
|
+
const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
|
|
19068
|
+
collections.push(...nestedCollections);
|
|
18796
19069
|
}
|
|
18797
|
-
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
18798
|
-
});
|
|
18799
|
-
throw mergeError;
|
|
18800
19070
|
}
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
await invalidateBranchContentCaches2(branchPath);
|
|
19071
|
+
return collections;
|
|
19072
|
+
} catch (err2) {
|
|
19073
|
+
if (err2.code === "ENOENT") {
|
|
19074
|
+
return [];
|
|
19075
|
+
}
|
|
19076
|
+
throw err2;
|
|
18808
19077
|
}
|
|
18809
19078
|
}
|
|
18810
|
-
async function
|
|
18811
|
-
|
|
18812
|
-
const
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
return;
|
|
19079
|
+
async function loadCollectionMetaFiles2(contentRoot) {
|
|
19080
|
+
let root = null;
|
|
19081
|
+
const rootMetaPath = join3(contentRoot, ".collection.json");
|
|
19082
|
+
try {
|
|
19083
|
+
await fs35.access(rootMetaPath);
|
|
19084
|
+
} catch (err2) {
|
|
19085
|
+
if (err2.code === "ENOENT") {
|
|
19086
|
+
} else {
|
|
19087
|
+
throw err2;
|
|
19088
|
+
}
|
|
18821
19089
|
}
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
-
|
|
18831
|
-
const result = await syncBoth(options);
|
|
18832
|
-
pushed = result.pushed;
|
|
18833
|
-
pulled = result.pulled;
|
|
18834
|
-
} else if (options.direction === "push") {
|
|
18835
|
-
const result = await syncPush(options);
|
|
18836
|
-
pushed = result.fileCount;
|
|
18837
|
-
} else {
|
|
18838
|
-
const result = await syncPull(options);
|
|
18839
|
-
pulled = result.fileCount;
|
|
19090
|
+
try {
|
|
19091
|
+
const content = await fs35.readFile(rootMetaPath, "utf-8");
|
|
19092
|
+
const parsed = JSON.parse(content);
|
|
19093
|
+
root = rootCollectionMetaSchema2.parse(parsed);
|
|
19094
|
+
} catch (err2) {
|
|
19095
|
+
const errno = err2.code;
|
|
19096
|
+
if (errno !== "ENOENT") {
|
|
19097
|
+
throw new Error(`Invalid root .collection.json`);
|
|
19098
|
+
}
|
|
18840
19099
|
}
|
|
18841
|
-
|
|
18842
|
-
return {
|
|
19100
|
+
const collections = await scanForCollectionMeta2(contentRoot);
|
|
19101
|
+
return { root, collections };
|
|
18843
19102
|
}
|
|
18844
|
-
var
|
|
18845
|
-
var
|
|
18846
|
-
"dist/
|
|
19103
|
+
var entryTypeMetaSchema2, collectionMetaSchema2, rootCollectionMetaSchema2;
|
|
19104
|
+
var init_meta_loader = __esm({
|
|
19105
|
+
"dist/schema/meta-loader.js"() {
|
|
18847
19106
|
"use strict";
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
19107
|
+
init_logger3();
|
|
19108
|
+
init_content_id_index();
|
|
19109
|
+
entryTypeMetaSchema2 = z18.object({
|
|
19110
|
+
name: z18.string().min(1),
|
|
19111
|
+
format: z18.enum(["md", "mdx", "json", "yaml"]),
|
|
19112
|
+
schema: z18.string().min(1),
|
|
19113
|
+
// Entry schema registry key (validated at resolution time)
|
|
19114
|
+
label: z18.string().optional(),
|
|
19115
|
+
description: z18.string().optional(),
|
|
19116
|
+
default: z18.boolean().optional(),
|
|
19117
|
+
maxItems: z18.number().int().positive().optional()
|
|
19118
|
+
});
|
|
19119
|
+
collectionMetaSchema2 = z18.object({
|
|
19120
|
+
name: z18.string().min(1),
|
|
19121
|
+
label: z18.string().optional(),
|
|
19122
|
+
description: z18.string().optional(),
|
|
19123
|
+
entries: z18.array(entryTypeMetaSchema2).optional(),
|
|
19124
|
+
order: z18.array(z18.string()).default([])
|
|
19125
|
+
// Embedded IDs for ordering; optional in the file, [] = alphabetical
|
|
19126
|
+
}).refine((data) => data.entries && data.entries.length > 0, {
|
|
19127
|
+
message: "Collection must have at least one entry type"
|
|
19128
|
+
});
|
|
19129
|
+
rootCollectionMetaSchema2 = z18.object({
|
|
19130
|
+
label: z18.string().optional(),
|
|
19131
|
+
entries: z18.array(entryTypeMetaSchema2).optional(),
|
|
19132
|
+
order: z18.array(z18.string()).optional()
|
|
19133
|
+
// Embedded IDs for ordering items
|
|
19134
|
+
});
|
|
18861
19135
|
}
|
|
18862
19136
|
});
|
|
18863
19137
|
|
|
18864
|
-
// dist/
|
|
18865
|
-
|
|
18866
|
-
|
|
19138
|
+
// dist/branch-schema-cache.js
|
|
19139
|
+
import { LRUCache as LRUCache2 } from "lru-cache";
|
|
19140
|
+
var SNAPSHOT_MEMO_MAX_BYTES2, SCHEMA_GENERATION_RESOURCE2;
|
|
19141
|
+
var init_branch_schema_cache = __esm({
|
|
19142
|
+
"dist/branch-schema-cache.js"() {
|
|
18867
19143
|
"use strict";
|
|
18868
|
-
|
|
19144
|
+
SNAPSHOT_MEMO_MAX_BYTES2 = 64 * 1024 * 1024;
|
|
19145
|
+
SCHEMA_GENERATION_RESOURCE2 = "schema";
|
|
18869
19146
|
}
|
|
18870
19147
|
});
|
|
18871
19148
|
|
|
18872
|
-
// dist/
|
|
18873
|
-
|
|
18874
|
-
|
|
19149
|
+
// dist/content-index-generation.js
|
|
19150
|
+
async function bumpContentIndexGeneration2(root) {
|
|
19151
|
+
return bumpResourceGeneration2(root, RESOURCE4);
|
|
19152
|
+
}
|
|
19153
|
+
async function invalidateBranchContentCaches2(root) {
|
|
19154
|
+
await bumpContentIndexGeneration2(root);
|
|
19155
|
+
await bumpResourceGeneration2(root, SCHEMA_GENERATION_RESOURCE2);
|
|
19156
|
+
invalidateContentIndexesForRoot2(root);
|
|
19157
|
+
}
|
|
19158
|
+
var RESOURCE4;
|
|
19159
|
+
var init_content_index_generation = __esm({
|
|
19160
|
+
"dist/content-index-generation.js"() {
|
|
18875
19161
|
"use strict";
|
|
18876
|
-
|
|
19162
|
+
init_resource_generation();
|
|
19163
|
+
init_content_index_registry();
|
|
19164
|
+
init_branch_schema_cache();
|
|
19165
|
+
RESOURCE4 = "content-index";
|
|
18877
19166
|
}
|
|
18878
19167
|
});
|
|
18879
19168
|
|
|
18880
|
-
// dist/utils/
|
|
18881
|
-
import
|
|
18882
|
-
import
|
|
18883
|
-
|
|
18884
|
-
|
|
18885
|
-
|
|
18886
|
-
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
const
|
|
18891
|
-
|
|
18892
|
-
await fs35.mkdir(dir, { recursive: true });
|
|
18893
|
-
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
18894
|
-
await fs35.writeFile(tempPath, content, "utf-8");
|
|
18895
|
-
if (expectedVersion === null) {
|
|
19169
|
+
// dist/utils/content-write-lock.js
|
|
19170
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
19171
|
+
import path44 from "node:path";
|
|
19172
|
+
function lockTargetDir2(branchRoot) {
|
|
19173
|
+
return path44.join(path44.resolve(branchRoot), META_DIR4);
|
|
19174
|
+
}
|
|
19175
|
+
function tryAcquireContentWriteLock2(branchRoot, onCompromised) {
|
|
19176
|
+
return tryAcquireProvisioningLock2(lockTargetDir2(branchRoot), CONTENT_WRITE_LOCK_NAME2, onCompromised, CONTENT_WRITE_LOCK_STALE_MS2, false);
|
|
19177
|
+
}
|
|
19178
|
+
async function acquireContentWriteLock2(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2, onCompromised) {
|
|
19179
|
+
const startedAt = Date.now();
|
|
19180
|
+
for (let attempt = 0; ; attempt++) {
|
|
18896
19181
|
try {
|
|
18897
|
-
await
|
|
19182
|
+
return await tryAcquireContentWriteLock2(branchRoot, onCompromised);
|
|
18898
19183
|
} catch (err2) {
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
19184
|
+
if (!isNodeError(err2) || err2.code !== "ELOCKED")
|
|
19185
|
+
throw err2;
|
|
19186
|
+
const remaining = waitMs - (Date.now() - startedAt);
|
|
19187
|
+
if (remaining <= 0)
|
|
19188
|
+
throw new ContentWriteLockBusyError2();
|
|
19189
|
+
const base = Math.min(40 * 2 ** attempt, 250);
|
|
19190
|
+
await sleep3(Math.min(base * (0.5 + Math.random()), remaining));
|
|
18905
19191
|
}
|
|
18906
|
-
|
|
19192
|
+
}
|
|
19193
|
+
}
|
|
19194
|
+
async function withContentWriteLock2(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
19195
|
+
let compromised = false;
|
|
19196
|
+
const release = await acquireContentWriteLock2(branchRoot, waitMs, (err2) => {
|
|
19197
|
+
compromised = true;
|
|
19198
|
+
canopyLogWarn3(`[canopy] Content-write lock compromised mid-hold for ${branchRoot}:`, getErrorMessage(err2));
|
|
19199
|
+
});
|
|
19200
|
+
let result;
|
|
19201
|
+
try {
|
|
19202
|
+
const held = new Set(heldRoots2.getStore());
|
|
19203
|
+
held.add(path44.resolve(branchRoot));
|
|
19204
|
+
result = await heldRoots2.run(held, fn);
|
|
19205
|
+
} finally {
|
|
19206
|
+
await release();
|
|
19207
|
+
}
|
|
19208
|
+
if (compromised) {
|
|
19209
|
+
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");
|
|
19210
|
+
}
|
|
19211
|
+
return result;
|
|
19212
|
+
}
|
|
19213
|
+
var META_DIR4, CONTENT_WRITE_LOCK_NAME2, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2, ContentWriteLockBusyError2, sleep3, CONTENT_WRITE_LOCK_STALE_MS2, heldRoots2;
|
|
19214
|
+
var init_content_write_lock = __esm({
|
|
19215
|
+
"dist/utils/content-write-lock.js"() {
|
|
19216
|
+
"use strict";
|
|
19217
|
+
init_error();
|
|
19218
|
+
init_logger3();
|
|
19219
|
+
init_provisioning_lock();
|
|
19220
|
+
META_DIR4 = ".canopy-meta";
|
|
19221
|
+
CONTENT_WRITE_LOCK_NAME2 = "content-write.lock";
|
|
19222
|
+
DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2 = 2e3;
|
|
19223
|
+
ContentWriteLockBusyError2 = class extends Error {
|
|
19224
|
+
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") {
|
|
19225
|
+
super(message);
|
|
19226
|
+
this.outcome = outcome;
|
|
19227
|
+
this.name = "ContentWriteLockBusyError";
|
|
19228
|
+
}
|
|
19229
|
+
};
|
|
19230
|
+
sleep3 = (ms) => new Promise((resolve2) => {
|
|
19231
|
+
setTimeout(resolve2, ms);
|
|
18907
19232
|
});
|
|
18908
|
-
|
|
19233
|
+
CONTENT_WRITE_LOCK_STALE_MS2 = 3e4;
|
|
19234
|
+
heldRoots2 = new AsyncLocalStorage3();
|
|
19235
|
+
}
|
|
19236
|
+
});
|
|
19237
|
+
|
|
19238
|
+
// dist/sync-core.js
|
|
19239
|
+
import fs36 from "node:fs/promises";
|
|
19240
|
+
import path45 from "node:path";
|
|
19241
|
+
import { simpleGit as simpleGit5 } from "simple-git";
|
|
19242
|
+
function assertWithinDir(resolved, parent, label) {
|
|
19243
|
+
const normalizedResolved = path45.resolve(resolved);
|
|
19244
|
+
const normalizedParent = path45.resolve(parent);
|
|
19245
|
+
if (!normalizedResolved.startsWith(normalizedParent + path45.sep) && normalizedResolved !== normalizedParent) {
|
|
19246
|
+
throw new Error(`${label} escapes the expected directory: ${resolved}`);
|
|
19247
|
+
}
|
|
19248
|
+
}
|
|
19249
|
+
async function safeReplaceDir(oldDir, newDir) {
|
|
19250
|
+
const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
19251
|
+
const oldExists = await filePathExists(oldDir);
|
|
19252
|
+
if (oldExists) {
|
|
19253
|
+
await fs36.rename(oldDir, backupDir);
|
|
18909
19254
|
}
|
|
18910
19255
|
try {
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
currentVersion = null;
|
|
18917
|
-
}
|
|
18918
|
-
if (currentVersion !== expectedVersion) {
|
|
18919
|
-
throw new OccWriteConflictError2();
|
|
19256
|
+
await fs36.rename(newDir, oldDir);
|
|
19257
|
+
} catch (err2) {
|
|
19258
|
+
if (oldExists) {
|
|
19259
|
+
await fs36.rename(backupDir, oldDir).catch(() => {
|
|
19260
|
+
});
|
|
18920
19261
|
}
|
|
18921
|
-
|
|
18922
|
-
|
|
18923
|
-
|
|
19262
|
+
throw err2;
|
|
19263
|
+
}
|
|
19264
|
+
if (oldExists) {
|
|
19265
|
+
await fs36.rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
19266
|
+
});
|
|
19267
|
+
}
|
|
19268
|
+
}
|
|
19269
|
+
async function listFilesRecursive(dir, prefix = "") {
|
|
19270
|
+
const results = [];
|
|
19271
|
+
let entries;
|
|
19272
|
+
try {
|
|
19273
|
+
entries = await fs36.readdir(dir, { withFileTypes: true });
|
|
19274
|
+
} catch {
|
|
19275
|
+
return results;
|
|
19276
|
+
}
|
|
19277
|
+
for (const entry of entries) {
|
|
19278
|
+
if (entry.name === ".git")
|
|
19279
|
+
continue;
|
|
19280
|
+
if (entry.isSymbolicLink())
|
|
19281
|
+
continue;
|
|
19282
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
19283
|
+
if (entry.isDirectory()) {
|
|
19284
|
+
results.push(...await listFilesRecursive(path45.join(dir, entry.name), rel));
|
|
19285
|
+
} else {
|
|
19286
|
+
results.push(rel);
|
|
18924
19287
|
}
|
|
18925
|
-
|
|
18926
|
-
|
|
18927
|
-
|
|
19288
|
+
}
|
|
19289
|
+
return results;
|
|
19290
|
+
}
|
|
19291
|
+
async function copyDir(src, dest) {
|
|
19292
|
+
await fs36.mkdir(dest, { recursive: true });
|
|
19293
|
+
const entries = await fs36.readdir(src, { withFileTypes: true });
|
|
19294
|
+
for (const entry of entries) {
|
|
19295
|
+
if (entry.name === ".git")
|
|
19296
|
+
continue;
|
|
19297
|
+
if (entry.isSymbolicLink())
|
|
19298
|
+
continue;
|
|
19299
|
+
const srcPath = path45.join(src, entry.name);
|
|
19300
|
+
const destPath = path45.join(dest, entry.name);
|
|
19301
|
+
if (entry.isDirectory()) {
|
|
19302
|
+
await copyDir(srcPath, destPath);
|
|
19303
|
+
} else {
|
|
19304
|
+
await fs36.copyFile(srcPath, destPath);
|
|
18928
19305
|
}
|
|
18929
|
-
} catch (err2) {
|
|
18930
|
-
await fs35.unlink(tempPath).catch(() => {
|
|
18931
|
-
});
|
|
18932
|
-
throw err2;
|
|
18933
19306
|
}
|
|
18934
|
-
return { version: newVersion, writeId };
|
|
18935
19307
|
}
|
|
18936
|
-
async function
|
|
18937
|
-
const
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
18944
|
-
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
19308
|
+
async function pushContentToWorkspace(options) {
|
|
19309
|
+
const { srcContentDir, branchPath, contentRoot } = options;
|
|
19310
|
+
if (!await filePathExists(srcContentDir)) {
|
|
19311
|
+
return { fileCount: 0, savedEditorState: false };
|
|
19312
|
+
}
|
|
19313
|
+
const wsContentDir = path45.join(branchPath, contentRoot);
|
|
19314
|
+
assertWithinDir(wsContentDir, branchPath, "content-root");
|
|
19315
|
+
return withContentWriteLock2(branchPath, () => replaceWorkspaceContent(options, wsContentDir));
|
|
19316
|
+
}
|
|
19317
|
+
async function replaceWorkspaceContent(options, wsContentDir) {
|
|
19318
|
+
const { srcContentDir, branchPath, commitMessage, baseTag, saveEditorStateMessage } = options;
|
|
19319
|
+
const wsGit = simpleGit5({ baseDir: branchPath });
|
|
19320
|
+
let savedEditorState = false;
|
|
19321
|
+
if (saveEditorStateMessage) {
|
|
19322
|
+
const editorChanges = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19323
|
+
if (editorChanges.length > 0) {
|
|
19324
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19325
|
+
await wsGit.commit(saveEditorStateMessage);
|
|
19326
|
+
savedEditorState = true;
|
|
18951
19327
|
}
|
|
18952
19328
|
}
|
|
18953
|
-
|
|
18954
|
-
}
|
|
18955
|
-
async function withOccFileLock2(filePath, fn) {
|
|
19329
|
+
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
18956
19330
|
try {
|
|
18957
|
-
await
|
|
19331
|
+
await copyDir(srcContentDir, tmpDir);
|
|
19332
|
+
await safeReplaceDir(wsContentDir, tmpDir);
|
|
18958
19333
|
} catch (err2) {
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
}
|
|
18963
|
-
const MAX_ATTEMPTS = 14;
|
|
18964
|
-
let release = null;
|
|
18965
|
-
for (let attempt = 1; release === null; attempt++) {
|
|
18966
|
-
try {
|
|
18967
|
-
release = await lockfile4.lock(filePath, {
|
|
18968
|
-
lockfilePath: `${filePath}.lock`,
|
|
18969
|
-
realpath: false,
|
|
18970
|
-
stale: 1e4,
|
|
18971
|
-
retries: 0,
|
|
18972
|
-
// A compromised lock (the lock dir vanished or refresh failed mid-hold, e.g. the branch
|
|
18973
|
-
// directory containing it was deleted, or an EFS waiter misread a cached mtime and took
|
|
18974
|
-
// it over) must not crash the process, which is proper-lockfile's default. Our critical
|
|
18975
|
-
// sections are short and idempotent-on-conflict (OCC verify inside); log and let the
|
|
18976
|
-
// section finish.
|
|
18977
|
-
onCompromised: guardOnCompromised2((err2) => canopyLogWarn3(`[canopy] OCC file lock compromised mid-hold for ${filePath}:`, getErrorMessage(err2)), filePath)
|
|
18978
|
-
});
|
|
18979
|
-
} catch (err2) {
|
|
18980
|
-
const code = isNodeError(err2) ? err2.code : void 0;
|
|
18981
|
-
const contended = code === "ELOCKED";
|
|
18982
|
-
if (!contended || attempt >= MAX_ATTEMPTS) {
|
|
18983
|
-
throw new OccWriteConflictError2(`Could not acquire file lock: ${getErrorMessage(err2)}`);
|
|
18984
|
-
}
|
|
18985
|
-
const baseDelay = Math.min(50 * Math.pow(1.6, attempt - 1), 2e3);
|
|
18986
|
-
const jitter = Math.random() * baseDelay;
|
|
18987
|
-
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
18988
|
-
}
|
|
19334
|
+
await fs36.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
19335
|
+
});
|
|
19336
|
+
throw err2;
|
|
18989
19337
|
}
|
|
18990
19338
|
try {
|
|
18991
|
-
|
|
19339
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19340
|
+
const staged = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19341
|
+
if (staged.length === 0) {
|
|
19342
|
+
if (baseTag)
|
|
19343
|
+
await wsGit.tag(["-f", baseTag]);
|
|
19344
|
+
return { fileCount: 0, savedEditorState };
|
|
19345
|
+
}
|
|
19346
|
+
await wsGit.commit(commitMessage ?? "sync: update content from working tree");
|
|
19347
|
+
if (baseTag)
|
|
19348
|
+
await wsGit.tag(["-f", baseTag]);
|
|
19349
|
+
return { fileCount: staged.length, savedEditorState };
|
|
18992
19350
|
} finally {
|
|
18993
|
-
await
|
|
18994
|
-
if (isNodeError(err2) && err2.code === "ENOENT")
|
|
18995
|
-
return;
|
|
18996
|
-
log14.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err2) });
|
|
18997
|
-
});
|
|
19351
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
18998
19352
|
}
|
|
18999
19353
|
}
|
|
19000
|
-
var
|
|
19001
|
-
var
|
|
19002
|
-
"dist/
|
|
19354
|
+
var SYNC_BASE_TAG;
|
|
19355
|
+
var init_sync_core = __esm({
|
|
19356
|
+
"dist/sync-core.js"() {
|
|
19003
19357
|
"use strict";
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
19008
|
-
|
|
19009
|
-
OccWriteConflictError2 = class extends Error {
|
|
19010
|
-
constructor(message = "Concurrent modification detected") {
|
|
19011
|
-
super(message);
|
|
19012
|
-
this.name = "OccWriteConflictError";
|
|
19013
|
-
}
|
|
19014
|
-
};
|
|
19358
|
+
init_content_index_generation();
|
|
19359
|
+
init_content_write_lock();
|
|
19360
|
+
init_fs();
|
|
19361
|
+
init_git();
|
|
19362
|
+
SYNC_BASE_TAG = "canopycms-sync-base";
|
|
19015
19363
|
}
|
|
19016
19364
|
});
|
|
19017
19365
|
|
|
19018
|
-
// dist/
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
});
|
|
19025
|
-
|
|
19026
|
-
await prev;
|
|
19366
|
+
// dist/git-manager.js
|
|
19367
|
+
import fs37 from "node:fs/promises";
|
|
19368
|
+
import path46 from "node:path";
|
|
19369
|
+
import { simpleGit as simpleGit6 } from "simple-git";
|
|
19370
|
+
async function ensureGitExcludePattern2(repoPath, pattern) {
|
|
19371
|
+
const excludePath = path46.join(repoPath, ".git", "info", "exclude");
|
|
19372
|
+
await fs37.mkdir(path46.dirname(excludePath), { recursive: true });
|
|
19373
|
+
let content = "";
|
|
19027
19374
|
try {
|
|
19028
|
-
|
|
19029
|
-
}
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
locks2.delete(key);
|
|
19375
|
+
content = await fs37.readFile(excludePath, "utf-8");
|
|
19376
|
+
} catch (err2) {
|
|
19377
|
+
if (!isNotFoundError(err2))
|
|
19378
|
+
throw err2;
|
|
19033
19379
|
}
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19037
|
-
|
|
19038
|
-
"use strict";
|
|
19039
|
-
locks2 = /* @__PURE__ */ new Map();
|
|
19380
|
+
const lines = content.split("\n");
|
|
19381
|
+
if (lines.some((line) => line.trim() === pattern)) {
|
|
19382
|
+
log14.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
19383
|
+
return;
|
|
19040
19384
|
}
|
|
19041
|
-
|
|
19042
|
-
|
|
19043
|
-
|
|
19044
|
-
|
|
19045
|
-
import { z as z17 } from "zod";
|
|
19046
|
-
async function withBranchSchemaLock(branchRoot, fn, contentWriteLockWaitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
19047
|
-
const schemaLockPath = path43.join(path43.resolve(branchRoot), ".canopy-meta", "schema");
|
|
19048
|
-
return withContentWriteLock2(branchRoot, () => withLock2(schemaLockPath, () => withOccFileLock2(schemaLockPath, fn)), contentWriteLockWaitMs);
|
|
19385
|
+
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
19386
|
+
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
19387
|
+
await fs37.writeFile(excludePath, newContent, "utf-8");
|
|
19388
|
+
log14.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
19049
19389
|
}
|
|
19050
|
-
var
|
|
19051
|
-
var
|
|
19052
|
-
"dist/
|
|
19390
|
+
var log14;
|
|
19391
|
+
var init_git_manager = __esm({
|
|
19392
|
+
"dist/git-manager.js"() {
|
|
19053
19393
|
"use strict";
|
|
19054
|
-
init_async_mutex();
|
|
19055
19394
|
init_debug2();
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
MAX_NAME_LENGTH = 64;
|
|
19059
|
-
MAX_LABEL_LENGTH = 128;
|
|
19060
|
-
SAFE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
19061
|
-
SAFE_NAME_MESSAGE = "must start with a letter and contain only lowercase letters, numbers, and hyphens";
|
|
19062
|
-
entryTypeInputSchema = z17.object({
|
|
19063
|
-
name: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Entry type name ${SAFE_NAME_MESSAGE}`),
|
|
19064
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19065
|
-
format: z17.enum(["md", "mdx", "json", "yaml"]),
|
|
19066
|
-
schema: z17.string().min(1),
|
|
19067
|
-
default: z17.boolean().optional(),
|
|
19068
|
-
maxItems: z17.number().int().positive().optional()
|
|
19069
|
-
});
|
|
19070
|
-
createCollectionInputSchema = z17.object({
|
|
19071
|
-
name: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`),
|
|
19072
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19073
|
-
parentPath: z17.string().optional(),
|
|
19074
|
-
entries: z17.array(entryTypeInputSchema).min(1, "Collection must have at least one entry type")
|
|
19075
|
-
});
|
|
19076
|
-
updateCollectionInputSchema = z17.object({
|
|
19077
|
-
name: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`).optional(),
|
|
19078
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19079
|
-
// Directory name (e.g., "posts" in "posts.{id}/")
|
|
19080
|
-
slug: z17.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Slug ${SAFE_NAME_MESSAGE}`).optional(),
|
|
19081
|
-
order: z17.array(z17.string()).optional()
|
|
19082
|
-
});
|
|
19083
|
-
updateEntryTypeInputSchema = z17.object({
|
|
19084
|
-
label: z17.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19085
|
-
format: z17.enum(["md", "mdx", "json", "yaml"]).optional(),
|
|
19086
|
-
schema: z17.string().min(1).optional(),
|
|
19087
|
-
default: z17.boolean().optional(),
|
|
19088
|
-
maxItems: z17.number().int().positive().optional()
|
|
19089
|
-
});
|
|
19090
|
-
log15 = createDebugLogger3({ prefix: "SchemaOps" });
|
|
19395
|
+
init_error();
|
|
19396
|
+
log14 = createDebugLogger3({ prefix: "GitManager" });
|
|
19091
19397
|
}
|
|
19092
19398
|
});
|
|
19093
19399
|
|
|
19094
|
-
// dist/
|
|
19095
|
-
var
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
"use strict";
|
|
19099
|
-
BranchMetadataCorruptError2 = class extends Error {
|
|
19100
|
-
constructor(branchRoot, cause) {
|
|
19101
|
-
super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
|
|
19102
|
-
this.name = "BranchMetadataCorruptError";
|
|
19103
|
-
this.branchRoot = branchRoot;
|
|
19104
|
-
this.parseCause = cause;
|
|
19105
|
-
}
|
|
19106
|
-
};
|
|
19107
|
-
}
|
|
19400
|
+
// dist/cli/sync.js
|
|
19401
|
+
var sync_exports = {};
|
|
19402
|
+
__export(sync_exports, {
|
|
19403
|
+
sync: () => sync
|
|
19108
19404
|
});
|
|
19109
|
-
|
|
19110
|
-
|
|
19111
|
-
import
|
|
19112
|
-
import
|
|
19113
|
-
import { z as
|
|
19114
|
-
function
|
|
19115
|
-
|
|
19116
|
-
|
|
19117
|
-
|
|
19118
|
-
|
|
19119
|
-
|
|
19120
|
-
|
|
19405
|
+
import fs38 from "node:fs/promises";
|
|
19406
|
+
import path47 from "node:path";
|
|
19407
|
+
import { simpleGit as simpleGit7 } from "simple-git";
|
|
19408
|
+
import * as p2 from "@clack/prompts";
|
|
19409
|
+
import { z as z19 } from "zod";
|
|
19410
|
+
function lockBusyAsSyncError(err2, branchName) {
|
|
19411
|
+
if (!(err2 instanceof ContentWriteLockBusyError2))
|
|
19412
|
+
return err2;
|
|
19413
|
+
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.`);
|
|
19414
|
+
}
|
|
19415
|
+
async function withWorkspaceLock(branchPath, branchName, fn) {
|
|
19416
|
+
try {
|
|
19417
|
+
return await withContentWriteLock2(branchPath, fn);
|
|
19418
|
+
} catch (err2) {
|
|
19419
|
+
throw lockBusyAsSyncError(err2, branchName);
|
|
19121
19420
|
}
|
|
19122
|
-
const parts = [];
|
|
19123
|
-
if (missing.size > 0)
|
|
19124
|
-
parts.push(`Missing: ${[...missing].join(", ")}`);
|
|
19125
|
-
if (invalid.size > 0)
|
|
19126
|
-
parts.push(`Invalid: ${[...invalid].join(", ")}`);
|
|
19127
|
-
return `Not branch metadata. ${parts.join(". ")}`;
|
|
19128
19421
|
}
|
|
19129
|
-
async function
|
|
19130
|
-
|
|
19131
|
-
|
|
19422
|
+
async function abortWorkspaceMerge(branchPath, branchName) {
|
|
19423
|
+
return withWorkspaceLock(branchPath, branchName, async () => {
|
|
19424
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19425
|
+
if ((await wsGit.status()).conflicted.length === 0)
|
|
19426
|
+
return false;
|
|
19427
|
+
await wsGit.merge(["--abort"]);
|
|
19428
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
19429
|
+
return true;
|
|
19430
|
+
});
|
|
19431
|
+
}
|
|
19432
|
+
async function detectCurrentBranch(projectDir) {
|
|
19433
|
+
const git = simpleGit7({ baseDir: projectDir });
|
|
19434
|
+
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
19435
|
+
if (branch === "HEAD")
|
|
19436
|
+
throw new Error("Detached HEAD \u2014 use --branch to specify a branch name");
|
|
19437
|
+
return branch;
|
|
19438
|
+
}
|
|
19439
|
+
async function readBranchConfigFields(projectDir) {
|
|
19440
|
+
if (!await filePathExists(path47.join(projectDir, "canopycms.config.ts")))
|
|
19441
|
+
return {};
|
|
19442
|
+
let why;
|
|
19132
19443
|
try {
|
|
19133
|
-
|
|
19444
|
+
const parsed = branchConfigSchema.safeParse((await importSiteServerConfig(projectDir)).server);
|
|
19445
|
+
if (parsed.success)
|
|
19446
|
+
return parsed.data;
|
|
19447
|
+
why = parsed.error.message;
|
|
19134
19448
|
} catch (err2) {
|
|
19135
|
-
|
|
19449
|
+
why = getErrorMessage(err2);
|
|
19450
|
+
}
|
|
19451
|
+
p2.log.warn(`Could not use canopycms.config.ts (${why}); branch names resolve against the default branch settings.`);
|
|
19452
|
+
return {};
|
|
19453
|
+
}
|
|
19454
|
+
async function loadBranchParamConfig(projectDir) {
|
|
19455
|
+
const fields = await readBranchConfigFields(projectDir);
|
|
19456
|
+
const head = await detectHeadBranch2(projectDir, fields.defaultBaseBranch ?? "main");
|
|
19457
|
+
return {
|
|
19458
|
+
...fields,
|
|
19459
|
+
mode: "dev",
|
|
19460
|
+
defaultBaseBranch: fields.defaultBaseBranch ?? head,
|
|
19461
|
+
defaultActiveBranch: fields.defaultActiveBranch ?? head
|
|
19462
|
+
};
|
|
19463
|
+
}
|
|
19464
|
+
async function assertOwnRepository(branchPath) {
|
|
19465
|
+
const top = await simpleGit7({ baseDir: branchPath }).revparse(["--show-toplevel"]).then((out) => out.trim()).catch(() => "");
|
|
19466
|
+
if (top && await fs38.realpath(top) === await fs38.realpath(branchPath))
|
|
19467
|
+
return;
|
|
19468
|
+
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.`);
|
|
19469
|
+
}
|
|
19470
|
+
async function workspaceHead(branchPath) {
|
|
19471
|
+
return simpleGit7({ baseDir: branchPath }).raw(["symbolic-ref", "--short", "-q", "HEAD"]).then((out) => out.trim() || null).catch(() => null);
|
|
19472
|
+
}
|
|
19473
|
+
async function keyWorkspace(key, projectDir) {
|
|
19474
|
+
const branchPath = path47.join(operatingStrategy3("dev").getContentBranchesRoot(projectDir), key);
|
|
19475
|
+
await assertOwnRepository(branchPath);
|
|
19476
|
+
const meta = await readBranchMetadataFile2(branchPath);
|
|
19477
|
+
return { branchName: meta?.branch.name ?? await workspaceHead(branchPath) ?? key, branchPath };
|
|
19478
|
+
}
|
|
19479
|
+
function refOf(name) {
|
|
19480
|
+
if (!isExactBranchForm(name))
|
|
19481
|
+
return name;
|
|
19482
|
+
const ref = exactBranchName(name);
|
|
19483
|
+
if (ref === null)
|
|
19484
|
+
throw new SyncError(`"${name}" is not a valid branch name.`);
|
|
19485
|
+
return ref;
|
|
19486
|
+
}
|
|
19487
|
+
async function resolveSyncWorkspace(name, projectDir, keys, byRef) {
|
|
19488
|
+
const config = await loadBranchParamConfig(projectDir);
|
|
19489
|
+
const param = byRef ? exactBranchSegment(name) : name;
|
|
19490
|
+
const resolved = await resolveBranchParam(param, config, { basePathOverride: projectDir }).catch((err2) => {
|
|
19491
|
+
if (namesNoWorkspace(err2))
|
|
19492
|
+
return null;
|
|
19493
|
+
throw err2;
|
|
19494
|
+
});
|
|
19495
|
+
if (resolved) {
|
|
19496
|
+
const ref2 = refOf(name);
|
|
19497
|
+
if (resolved.branch.name !== ref2 && resolved.branchRoot === refKeyPath(ref2, projectDir)) {
|
|
19498
|
+
throw holdsAnotherBranch(resolved.branchRoot, resolved.branch.name, ref2);
|
|
19499
|
+
}
|
|
19500
|
+
await assertOwnRepository(resolved.branchRoot);
|
|
19501
|
+
return { branchName: resolved.branch.name, branchPath: resolved.branchRoot };
|
|
19502
|
+
}
|
|
19503
|
+
if (!byRef && keys.includes(name))
|
|
19504
|
+
return keyWorkspace(name, projectDir);
|
|
19505
|
+
const ref = refOf(name);
|
|
19506
|
+
const branchPath = refKeyPath(ref, projectDir);
|
|
19507
|
+
const present = await filePathExists(branchPath).catch((err2) => {
|
|
19508
|
+
if (namesNoWorkspace(err2))
|
|
19509
|
+
throw new SyncError(`"${ref}" cannot name a branch workspace.`);
|
|
19510
|
+
throw err2;
|
|
19511
|
+
});
|
|
19512
|
+
if (!present)
|
|
19513
|
+
return null;
|
|
19514
|
+
await assertOwnRepository(branchPath);
|
|
19515
|
+
const meta = await readBranchMetadataFile2(branchPath);
|
|
19516
|
+
const holder = meta ? meta.branch.name : await workspaceHead(branchPath);
|
|
19517
|
+
if (holder !== ref)
|
|
19518
|
+
throw holdsAnotherBranch(branchPath, holder, ref);
|
|
19519
|
+
return { branchName: ref, branchPath };
|
|
19520
|
+
}
|
|
19521
|
+
function holdsAnotherBranch(branchPath, holder, ref) {
|
|
19522
|
+
const recovery = holder?.startsWith("sync-incoming-") ? ` An interrupted "sync both" left it there: run "git checkout ${ref}" in that workspace.` : "";
|
|
19523
|
+
return new SyncError(`Branch workspace "${path47.basename(branchPath)}" holds ${holder === null ? "a detached HEAD" : `branch "${holder}"`}, not "${ref}".${recovery}`);
|
|
19524
|
+
}
|
|
19525
|
+
function refKeyPath(name, projectDir) {
|
|
19526
|
+
try {
|
|
19527
|
+
return resolveBranchPath2({ mode: "dev", branchName: name, basePathOverride: projectDir }).branchRoot;
|
|
19528
|
+
} catch (err2) {
|
|
19529
|
+
if (namesNoWorkspace(err2))
|
|
19530
|
+
throw new SyncError(`"${name}" cannot name a branch workspace.`);
|
|
19531
|
+
throw err2;
|
|
19532
|
+
}
|
|
19533
|
+
}
|
|
19534
|
+
async function createSyncWorkspace(name, projectDir) {
|
|
19535
|
+
const ref = refOf(name);
|
|
19536
|
+
const parsed = parseBranchRef3(ref);
|
|
19537
|
+
if (!parsed.ok)
|
|
19538
|
+
throw new SyncError(`"${ref}" is not a valid branch name: ${parsed.error}`);
|
|
19539
|
+
const branchPath = refKeyPath(ref, projectDir);
|
|
19540
|
+
await fs38.mkdir(branchPath, { recursive: true });
|
|
19541
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19542
|
+
await wsGit.init();
|
|
19543
|
+
await wsGit.checkoutLocalBranch(ref);
|
|
19544
|
+
await wsGit.raw(["commit", "--allow-empty", "-m", "init: workspace created by sync"]);
|
|
19545
|
+
await ensureGitExcludePattern2(branchPath, operatingStrategy3("dev").getGitExcludePattern());
|
|
19546
|
+
p2.log.info(`Created branch workspace: ${path47.basename(branchPath)}`);
|
|
19547
|
+
return { branchName: ref, branchPath };
|
|
19548
|
+
}
|
|
19549
|
+
async function selectBranch(options) {
|
|
19550
|
+
const branchesDir = operatingStrategy3("dev").getContentBranchesRoot(options.projectDir);
|
|
19551
|
+
let branches = [];
|
|
19552
|
+
if (await filePathExists(branchesDir)) {
|
|
19553
|
+
const entries = await fs38.readdir(branchesDir, { withFileTypes: true });
|
|
19554
|
+
branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
19555
|
+
}
|
|
19556
|
+
let branchName = options.branch;
|
|
19557
|
+
const byRef = !branchName;
|
|
19558
|
+
if (!branchName) {
|
|
19559
|
+
try {
|
|
19560
|
+
branchName = await detectCurrentBranch(options.projectDir);
|
|
19561
|
+
} catch {
|
|
19562
|
+
}
|
|
19563
|
+
}
|
|
19564
|
+
if (!branchName) {
|
|
19565
|
+
if (branches.length === 0) {
|
|
19566
|
+
throw new SyncError("No branch workspaces found and could not detect current git branch. Use --branch to specify.");
|
|
19567
|
+
}
|
|
19568
|
+
if (branches.length === 1)
|
|
19569
|
+
return keyWorkspace(branches[0], options.projectDir);
|
|
19570
|
+
const result = await p2.select({
|
|
19571
|
+
message: "Which branch?",
|
|
19572
|
+
options: branches.map((b) => ({ value: b, label: b }))
|
|
19573
|
+
});
|
|
19574
|
+
if (p2.isCancel(result)) {
|
|
19575
|
+
p2.cancel("Sync cancelled.");
|
|
19136
19576
|
return null;
|
|
19137
19577
|
}
|
|
19138
|
-
|
|
19578
|
+
return keyWorkspace(result, options.projectDir);
|
|
19139
19579
|
}
|
|
19140
|
-
|
|
19580
|
+
const existing = await resolveSyncWorkspace(branchName, options.projectDir, branches, byRef);
|
|
19581
|
+
if (existing)
|
|
19582
|
+
return existing;
|
|
19583
|
+
if (options.autoCreate)
|
|
19584
|
+
return createSyncWorkspace(branchName, options.projectDir);
|
|
19585
|
+
const available = branches.length > 0 ? ` Available branches: ${branches.join(", ")}` : "";
|
|
19586
|
+
throw new SyncError(`Branch workspace "${branchName}" not found.${available}`);
|
|
19141
19587
|
}
|
|
19142
|
-
function
|
|
19143
|
-
|
|
19588
|
+
async function syncPush(options) {
|
|
19589
|
+
const { projectDir } = options;
|
|
19590
|
+
const contentRoot = options.contentRoot || "content";
|
|
19591
|
+
const srcContentDir = path47.join(projectDir, contentRoot);
|
|
19592
|
+
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
19593
|
+
if (!await filePathExists(srcContentDir)) {
|
|
19594
|
+
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
19595
|
+
}
|
|
19596
|
+
const ws = await selectBranch({ ...options, projectDir, autoCreate: true });
|
|
19597
|
+
if (!ws)
|
|
19598
|
+
return { fileCount: 0 };
|
|
19599
|
+
const { branchName, branchPath } = ws;
|
|
19600
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19601
|
+
const status = await wsGit.status();
|
|
19602
|
+
if (status.conflicted.length > 0) {
|
|
19603
|
+
p2.log.error("Branch workspace has unresolved merge conflicts.");
|
|
19604
|
+
if (!options.force) {
|
|
19605
|
+
const shouldAbort = await p2.confirm({
|
|
19606
|
+
message: "Abort the merge and restore workspace to pre-merge state?",
|
|
19607
|
+
initialValue: false
|
|
19608
|
+
});
|
|
19609
|
+
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
19610
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
19611
|
+
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
19612
|
+
}
|
|
19613
|
+
}
|
|
19614
|
+
}
|
|
19615
|
+
return { fileCount: 0 };
|
|
19616
|
+
}
|
|
19617
|
+
const editorChanges = status.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19618
|
+
if (editorChanges.length > 0 && !options.force) {
|
|
19619
|
+
p2.log.warn(`Branch workspace has ${editorChanges.length} uncommitted change(s) that will be committed to history then overwritten:`);
|
|
19620
|
+
for (const file of editorChanges.slice(0, 10)) {
|
|
19621
|
+
p2.log.warn(` ${file.path}`);
|
|
19622
|
+
}
|
|
19623
|
+
if (editorChanges.length > 10) {
|
|
19624
|
+
p2.log.warn(` ... and ${editorChanges.length - 10} more`);
|
|
19625
|
+
}
|
|
19626
|
+
const confirm5 = await p2.confirm({
|
|
19627
|
+
message: "Continue? Editor changes will be preserved in git history.",
|
|
19628
|
+
initialValue: false
|
|
19629
|
+
});
|
|
19630
|
+
if (p2.isCancel(confirm5) || !confirm5) {
|
|
19631
|
+
p2.cancel("Push cancelled.");
|
|
19632
|
+
return { fileCount: 0 };
|
|
19633
|
+
}
|
|
19634
|
+
}
|
|
19635
|
+
p2.log.step(`Pushing content to branch workspace: ${branchName}`);
|
|
19636
|
+
let fileCount;
|
|
19144
19637
|
try {
|
|
19145
|
-
|
|
19638
|
+
const result = await pushContentToWorkspace({
|
|
19639
|
+
srcContentDir,
|
|
19640
|
+
branchPath,
|
|
19641
|
+
contentRoot,
|
|
19642
|
+
baseTag: SYNC_BASE_TAG,
|
|
19643
|
+
saveEditorStateMessage: "sync: save editor state before push"
|
|
19644
|
+
});
|
|
19645
|
+
fileCount = result.fileCount;
|
|
19646
|
+
if (result.savedEditorState)
|
|
19647
|
+
p2.log.info("Committed editor changes to history before push");
|
|
19146
19648
|
} catch (err2) {
|
|
19147
|
-
throw
|
|
19649
|
+
throw lockBusyAsSyncError(err2, branchName);
|
|
19148
19650
|
}
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19651
|
+
if (fileCount === 0) {
|
|
19652
|
+
p2.log.info("Content is already up to date \u2014 nothing to push");
|
|
19653
|
+
} else {
|
|
19654
|
+
p2.log.success(`Pushed ${fileCount} file change(s) to branch "${branchName}"`);
|
|
19152
19655
|
}
|
|
19153
|
-
|
|
19154
|
-
return file;
|
|
19656
|
+
return { fileCount };
|
|
19155
19657
|
}
|
|
19156
|
-
|
|
19157
|
-
|
|
19158
|
-
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
19162
|
-
|
|
19163
|
-
|
|
19164
|
-
|
|
19165
|
-
|
|
19166
|
-
|
|
19167
|
-
|
|
19168
|
-
|
|
19169
|
-
|
|
19170
|
-
description: z18.string().optional(),
|
|
19171
|
-
status: enumOf2({
|
|
19172
|
-
editing: "editing",
|
|
19173
|
-
submitted: "submitted",
|
|
19174
|
-
approved: "approved",
|
|
19175
|
-
archived: "archived"
|
|
19176
|
-
}),
|
|
19177
|
-
access: z18.object({
|
|
19178
|
-
allowedUsers: z18.array(z18.string()).optional(),
|
|
19179
|
-
allowedGroups: z18.array(z18.string()).optional(),
|
|
19180
|
-
managerOrAdminAllowed: z18.boolean().optional()
|
|
19181
|
-
}).passthrough(),
|
|
19182
|
-
createdBy: z18.string().default("unknown"),
|
|
19183
|
-
createdAt: z18.string().default(UNKNOWN_TIME2),
|
|
19184
|
-
updatedAt: z18.string().default(UNKNOWN_TIME2),
|
|
19185
|
-
baseBranch: z18.string().optional(),
|
|
19186
|
-
pullRequestUrl: z18.string().optional(),
|
|
19187
|
-
pullRequestNumber: z18.number().int().optional(),
|
|
19188
|
-
submittedAt: z18.string().optional(),
|
|
19189
|
-
pushedToGitHubAt: z18.string().optional(),
|
|
19190
|
-
editorUrl: z18.string().optional(),
|
|
19191
|
-
syncStatus: enumOf2({
|
|
19192
|
-
synced: "synced",
|
|
19193
|
-
"pending-sync": "pending-sync",
|
|
19194
|
-
"sync-failed": "sync-failed"
|
|
19195
|
-
}).optional(),
|
|
19196
|
-
conflictStatus: enumOf2({
|
|
19197
|
-
clean: "clean",
|
|
19198
|
-
"conflicts-detected": "conflicts-detected"
|
|
19199
|
-
}).optional(),
|
|
19200
|
-
conflictFiles: z18.array(z18.custom((v) => typeof v === "string")).optional(),
|
|
19201
|
-
pullRequestState: enumOf2({
|
|
19202
|
-
open: "open",
|
|
19203
|
-
closed: "closed",
|
|
19204
|
-
merged: "merged"
|
|
19205
|
-
}).optional(),
|
|
19206
|
-
mergedAt: z18.string().optional(),
|
|
19207
|
-
rebaseFailure: z18.object({ message: z18.string(), firstAt: z18.string(), lastAt: z18.string() }).optional(),
|
|
19208
|
-
historyRewrittenFrom: z18.string().optional(),
|
|
19209
|
-
syncFailureReason: z18.string().optional(),
|
|
19210
|
-
editors: z18.array(z18.string()).optional(),
|
|
19211
|
-
uncommittedEditors: z18.array(z18.string()).optional(),
|
|
19212
|
-
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
19213
|
-
review: z18.object({
|
|
19214
|
-
state: z18.enum(["approved", "changes-requested"]),
|
|
19215
|
-
by: z18.object({ userId: z18.string(), name: z18.string().optional() }),
|
|
19216
|
-
at: z18.string()
|
|
19217
|
-
}).optional().catch(void 0)
|
|
19218
|
-
};
|
|
19219
|
-
branchMetadataSchema2 = z18.object(branchMetadataShape2).passthrough();
|
|
19220
|
-
branchMetadataFileSchema2 = z18.object({
|
|
19221
|
-
schemaVersion: z18.number().int().default(1),
|
|
19222
|
-
version: z18.number().int().nonnegative().default(0),
|
|
19223
|
-
writeId: z18.string().optional(),
|
|
19224
|
-
branch: branchMetadataSchema2
|
|
19225
|
-
}).passthrough();
|
|
19658
|
+
async function syncPull(options) {
|
|
19659
|
+
const ws = await selectBranch({ ...options, projectDir: options.projectDir });
|
|
19660
|
+
if (!ws)
|
|
19661
|
+
return { fileCount: 0 };
|
|
19662
|
+
return pullFromWorkspace(options, ws);
|
|
19663
|
+
}
|
|
19664
|
+
async function pullFromWorkspace(options, ws) {
|
|
19665
|
+
const { projectDir } = options;
|
|
19666
|
+
const contentRoot = options.contentRoot || "content";
|
|
19667
|
+
const { branchName, branchPath } = ws;
|
|
19668
|
+
const branchContentDir = path47.join(branchPath, contentRoot);
|
|
19669
|
+
assertWithinDir(branchContentDir, branchPath, "--content-root");
|
|
19670
|
+
if (!await filePathExists(branchContentDir)) {
|
|
19671
|
+
throw new SyncError(`Content directory not found in branch workspace: ${path47.basename(branchPath)}/${contentRoot}`);
|
|
19226
19672
|
}
|
|
19227
|
-
});
|
|
19228
|
-
|
|
19229
|
-
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
|
|
19241
|
-
|
|
19242
|
-
log16 = createDebugLogger3({ prefix: "BranchRegistry" });
|
|
19243
|
-
REGISTRY_FILE2 = "branches.json";
|
|
19244
|
-
REGISTRY_STALE_FILE2 = "branches.stale.json";
|
|
19245
|
-
REGISTRY_TEMP_FILE2 = "branches.tmp.json";
|
|
19246
|
-
REGISTRY_VERSION2 = 2;
|
|
19247
|
-
RESOURCE4 = "branch-registry";
|
|
19248
|
-
GET_MISS_REFRESH_MIN_INTERVAL_MS2 = 5e3;
|
|
19249
|
-
snapshotSchema2 = z19.object({
|
|
19250
|
-
version: z19.literal(REGISTRY_VERSION2),
|
|
19251
|
-
branches: z19.array(z19.object({ branch: branchMetadataSchema2, branchRoot: z19.string(), baseRoot: z19.string() })),
|
|
19252
|
-
generation: z19.string().nullable()
|
|
19253
|
-
});
|
|
19254
|
-
reportedUnreadable2 = /* @__PURE__ */ new Set();
|
|
19255
|
-
BranchRegistry2 = class {
|
|
19256
|
-
constructor(root) {
|
|
19257
|
-
this.regenInFlight = null;
|
|
19258
|
-
this.lastForcedRefreshMs = 0;
|
|
19259
|
-
this.root = path45.resolve(root);
|
|
19260
|
-
this.registryPath = path45.join(this.root, REGISTRY_FILE2);
|
|
19261
|
-
this.stalePath = path45.join(this.root, REGISTRY_STALE_FILE2);
|
|
19262
|
-
this.tempPath = path45.join(this.root, REGISTRY_TEMP_FILE2);
|
|
19263
|
-
}
|
|
19264
|
-
/**
|
|
19265
|
-
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
19266
|
-
* token matches the live marker, regenerates otherwise.
|
|
19267
|
-
*
|
|
19268
|
-
* An unparseable or wrong-shaped snapshot is treated like an absent one: the
|
|
19269
|
-
* snapshot is a cache of the branch directories, so rebuilding it loses
|
|
19270
|
-
* nothing, while rethrowing would fail every editor's branch listing until
|
|
19271
|
-
* someone deleted the file. Other read failures (permissions, IO) propagate.
|
|
19272
|
-
*/
|
|
19273
|
-
async list() {
|
|
19274
|
-
let raw;
|
|
19275
|
-
try {
|
|
19276
|
-
raw = await fs37.readFile(this.registryPath, "utf8");
|
|
19277
|
-
} catch (err2) {
|
|
19278
|
-
if (isNotFoundError(err2)) {
|
|
19279
|
-
return await this.regenerate();
|
|
19280
|
-
}
|
|
19281
|
-
throw err2;
|
|
19282
|
-
}
|
|
19283
|
-
const parsed = this.parseSnapshot(raw);
|
|
19284
|
-
if (!parsed)
|
|
19285
|
-
return await this.regenerate();
|
|
19286
|
-
const read = await readResourceGeneration2(this.root, RESOURCE4);
|
|
19287
|
-
if (isGenerationCurrent2(parsed.generation, read)) {
|
|
19288
|
-
return parsed.branches;
|
|
19289
|
-
}
|
|
19290
|
-
return await this.regenerate();
|
|
19291
|
-
}
|
|
19292
|
-
/** The snapshot in `raw`, or null when it must be rebuilt. */
|
|
19293
|
-
parseSnapshot(raw) {
|
|
19294
|
-
let json;
|
|
19295
|
-
try {
|
|
19296
|
-
json = JSON.parse(raw);
|
|
19297
|
-
} catch (err2) {
|
|
19298
|
-
this.reportUnreadable(getErrorMessage(err2));
|
|
19299
|
-
return null;
|
|
19300
|
-
}
|
|
19301
|
-
if (typeof json === "object" && json !== null && "version" in json && typeof json.version === "number" && json.version !== REGISTRY_VERSION2) {
|
|
19302
|
-
return null;
|
|
19303
|
-
}
|
|
19304
|
-
const result = snapshotSchema2.safeParse(json);
|
|
19305
|
-
if (!result.success) {
|
|
19306
|
-
this.reportUnreadable("it does not have the registry snapshot shape");
|
|
19307
|
-
return null;
|
|
19673
|
+
p2.log.step(`Pulling content from branch: ${branchName}`);
|
|
19674
|
+
const destContentDir = path47.join(projectDir, contentRoot);
|
|
19675
|
+
assertWithinDir(destContentDir, projectDir, "--content-root");
|
|
19676
|
+
const sourceGit = simpleGit7({ baseDir: projectDir });
|
|
19677
|
+
const status = await sourceGit.status();
|
|
19678
|
+
const uncommittedContent = status.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
19679
|
+
const untrackedLosses = [];
|
|
19680
|
+
if (await filePathExists(destContentDir)) {
|
|
19681
|
+
const localFiles = await listFilesRecursive(destContentDir);
|
|
19682
|
+
const branchFiles = new Set(await listFilesRecursive(branchContentDir));
|
|
19683
|
+
for (const file of localFiles) {
|
|
19684
|
+
if (!branchFiles.has(file)) {
|
|
19685
|
+
const relativePath = contentRoot + "/" + file;
|
|
19686
|
+
if (!uncommittedContent.some((f) => f.path === relativePath)) {
|
|
19687
|
+
untrackedLosses.push(relativePath);
|
|
19308
19688
|
}
|
|
19309
|
-
reportedUnreadable2.delete(this.registryPath);
|
|
19310
|
-
const snapshot = result.data;
|
|
19311
|
-
return snapshot;
|
|
19312
|
-
}
|
|
19313
|
-
reportUnreadable(reason) {
|
|
19314
|
-
if (reportedUnreadable2.has(this.registryPath))
|
|
19315
|
-
return;
|
|
19316
|
-
reportedUnreadable2.add(this.registryPath);
|
|
19317
|
-
canopyLogWarn3(`CanopyCMS: Branch registry snapshot ${this.registryPath} is unreadable (${reason}); regenerating it from the branch directories`);
|
|
19318
|
-
}
|
|
19319
|
-
/**
|
|
19320
|
-
* Returns a single branch by name, from the cached snapshot when it is fresh.
|
|
19321
|
-
*
|
|
19322
|
-
* Suspicious-miss backstop: a `name` missing from the list is the signal that
|
|
19323
|
-
* the snapshot may predate a branch that now exists, so force one fresh
|
|
19324
|
-
* regeneration and re-search before giving up. Throttled per instance, so a
|
|
19325
|
-
* genuinely-absent branch costs at most one extra scan per window.
|
|
19326
|
-
*/
|
|
19327
|
-
async get(name) {
|
|
19328
|
-
return this.find((b) => b.branch.name === name);
|
|
19329
|
-
}
|
|
19330
|
-
/**
|
|
19331
|
-
* Returns the branch whose workspace directory is `key` (`sanitizeBranchName` of its ref),
|
|
19332
|
-
* whatever its name, with {@link get}'s backstop.
|
|
19333
|
-
*/
|
|
19334
|
-
async getByKey(key) {
|
|
19335
|
-
return this.find((b) => path45.basename(b.branchRoot) === key);
|
|
19336
19689
|
}
|
|
19337
|
-
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19690
|
+
}
|
|
19691
|
+
}
|
|
19692
|
+
const totalWarnings = uncommittedContent.length + untrackedLosses.length;
|
|
19693
|
+
if (totalWarnings > 0 && !options.force) {
|
|
19694
|
+
if (uncommittedContent.length > 0) {
|
|
19695
|
+
p2.log.warn(`You have ${uncommittedContent.length} uncommitted change(s) in ${contentRoot}/:`);
|
|
19696
|
+
for (const file of uncommittedContent.slice(0, 10)) {
|
|
19697
|
+
p2.log.warn(` ${file.path}`);
|
|
19345
19698
|
}
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
if (now - this.lastForcedRefreshMs < GET_MISS_REFRESH_MIN_INTERVAL_MS2)
|
|
19349
|
-
return false;
|
|
19350
|
-
this.lastForcedRefreshMs = now;
|
|
19351
|
-
return true;
|
|
19699
|
+
if (uncommittedContent.length > 10) {
|
|
19700
|
+
p2.log.warn(` ... and ${uncommittedContent.length - 10} more`);
|
|
19352
19701
|
}
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
19357
|
-
|
|
19358
|
-
* bump alone already restored correctness for every future reader.
|
|
19359
|
-
*/
|
|
19360
|
-
async invalidate() {
|
|
19361
|
-
await bumpResourceGeneration2(this.root, RESOURCE4, { mustSucceed: true });
|
|
19362
|
-
try {
|
|
19363
|
-
if (this.regenInFlight)
|
|
19364
|
-
await this.regenInFlight.catch(() => {
|
|
19365
|
-
});
|
|
19366
|
-
await this.regenerate();
|
|
19367
|
-
} catch (err2) {
|
|
19368
|
-
log16.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
19369
|
-
error: getErrorMessage(err2)
|
|
19370
|
-
});
|
|
19371
|
-
}
|
|
19702
|
+
}
|
|
19703
|
+
if (untrackedLosses.length > 0) {
|
|
19704
|
+
p2.log.warn(`${untrackedLosses.length} file(s) in ${contentRoot}/ not present in branch "${branchName}" will be deleted:`);
|
|
19705
|
+
for (const file of untrackedLosses.slice(0, 10)) {
|
|
19706
|
+
p2.log.warn(` ${file}`);
|
|
19372
19707
|
}
|
|
19373
|
-
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
|
|
19380
|
-
|
|
19381
|
-
|
|
19382
|
-
|
|
19383
|
-
|
|
19384
|
-
|
|
19385
|
-
|
|
19386
|
-
|
|
19387
|
-
|
|
19708
|
+
if (untrackedLosses.length > 10) {
|
|
19709
|
+
p2.log.warn(` ... and ${untrackedLosses.length - 10} more`);
|
|
19710
|
+
}
|
|
19711
|
+
}
|
|
19712
|
+
const confirm5 = await p2.confirm({
|
|
19713
|
+
message: "Overwrite content directory? Files listed above will be lost.",
|
|
19714
|
+
initialValue: false
|
|
19715
|
+
});
|
|
19716
|
+
if (p2.isCancel(confirm5) || !confirm5) {
|
|
19717
|
+
p2.cancel("Pull cancelled. Commit or stash your changes first.");
|
|
19718
|
+
return { fileCount: 0 };
|
|
19719
|
+
}
|
|
19720
|
+
}
|
|
19721
|
+
const tmpDestDir = `${destContentDir}.sync-tmp-${Date.now()}`;
|
|
19722
|
+
try {
|
|
19723
|
+
await copyDir(branchContentDir, tmpDestDir);
|
|
19724
|
+
await safeReplaceDir(destContentDir, tmpDestDir);
|
|
19725
|
+
} catch (err2) {
|
|
19726
|
+
await fs38.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
|
|
19727
|
+
});
|
|
19728
|
+
throw err2;
|
|
19729
|
+
}
|
|
19730
|
+
const postStatus = await sourceGit.status();
|
|
19731
|
+
const contentChanges = postStatus.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
|
|
19732
|
+
if (contentChanges.length === 0) {
|
|
19733
|
+
p2.log.info("Content is already up to date \u2014 nothing to pull");
|
|
19734
|
+
} else {
|
|
19735
|
+
p2.log.success(`Pulled ${contentChanges.length} changed file(s) from branch "${branchName}"`);
|
|
19736
|
+
for (const file of contentChanges.slice(0, 20)) {
|
|
19737
|
+
const indicator = file.working_dir === "?" ? "A" : file.working_dir || file.index;
|
|
19738
|
+
p2.log.info(` ${indicator} ${file.path}`);
|
|
19739
|
+
}
|
|
19740
|
+
if (contentChanges.length > 20) {
|
|
19741
|
+
p2.log.info(` ... and ${contentChanges.length - 20} more`);
|
|
19742
|
+
}
|
|
19743
|
+
p2.log.info("\nReview the changes, then git add and commit when ready.");
|
|
19744
|
+
}
|
|
19745
|
+
return { fileCount: contentChanges.length };
|
|
19746
|
+
}
|
|
19747
|
+
async function syncBoth(options) {
|
|
19748
|
+
const { projectDir } = options;
|
|
19749
|
+
const contentRoot = options.contentRoot || "content";
|
|
19750
|
+
const srcContentDir = path47.join(projectDir, contentRoot);
|
|
19751
|
+
assertWithinDir(srcContentDir, projectDir, "--content-root");
|
|
19752
|
+
if (!await filePathExists(srcContentDir)) {
|
|
19753
|
+
throw new SyncError(`Content directory not found: ${contentRoot}/ (expected at ${srcContentDir})`);
|
|
19754
|
+
}
|
|
19755
|
+
const ws = await selectBranch({ ...options, projectDir, autoCreate: true });
|
|
19756
|
+
if (!ws)
|
|
19757
|
+
return { pushed: 0, pulled: 0 };
|
|
19758
|
+
const { branchName, branchPath } = ws;
|
|
19759
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19760
|
+
const status = await wsGit.status();
|
|
19761
|
+
if (status.conflicted.length > 0) {
|
|
19762
|
+
p2.log.error("Branch workspace has unresolved merge conflicts.");
|
|
19763
|
+
if (!options.force) {
|
|
19764
|
+
const shouldAbort = await p2.confirm({
|
|
19765
|
+
message: "Abort the merge and restore workspace to pre-merge state?",
|
|
19766
|
+
initialValue: false
|
|
19767
|
+
});
|
|
19768
|
+
if (!p2.isCancel(shouldAbort) && shouldAbort) {
|
|
19769
|
+
if (await abortWorkspaceMerge(branchPath, branchName)) {
|
|
19770
|
+
p2.log.success("Merge aborted. Workspace restored to pre-merge state.");
|
|
19388
19771
|
}
|
|
19389
19772
|
}
|
|
19390
|
-
|
|
19391
|
-
|
|
19392
|
-
|
|
19393
|
-
|
|
19773
|
+
}
|
|
19774
|
+
return { pushed: 0, pulled: 0 };
|
|
19775
|
+
}
|
|
19776
|
+
return withWorkspaceLock(branchPath, branchName, () => mergeIntoWorkspace(options, { branchName, branchPath, srcContentDir, contentRoot }));
|
|
19777
|
+
}
|
|
19778
|
+
async function mergeIntoWorkspace(options, ws) {
|
|
19779
|
+
const { branchPath, srcContentDir, contentRoot } = ws;
|
|
19780
|
+
const wsGit = simpleGit7({ baseDir: branchPath });
|
|
19781
|
+
const status = await wsGit.status();
|
|
19782
|
+
if (status.files.some((f) => !isCanopyInternalPath(f.path))) {
|
|
19783
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19784
|
+
await wsGit.commit("sync: save editor state before merge");
|
|
19785
|
+
p2.log.info("Committed editor changes before merge");
|
|
19786
|
+
}
|
|
19787
|
+
let baseRef;
|
|
19788
|
+
try {
|
|
19789
|
+
await wsGit.raw(["rev-parse", SYNC_BASE_TAG]);
|
|
19790
|
+
baseRef = SYNC_BASE_TAG;
|
|
19791
|
+
} catch {
|
|
19792
|
+
baseRef = "HEAD";
|
|
19793
|
+
}
|
|
19794
|
+
const currentBranch = (await wsGit.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
19795
|
+
try {
|
|
19796
|
+
const incomingBranch = `sync-incoming-${Date.now()}`;
|
|
19797
|
+
await wsGit.raw(["checkout", "-b", incomingBranch, baseRef]);
|
|
19798
|
+
const wsContentDir = path47.join(branchPath, contentRoot);
|
|
19799
|
+
assertWithinDir(wsContentDir, branchPath, "--content-root");
|
|
19800
|
+
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
19801
|
+
try {
|
|
19802
|
+
await copyDir(srcContentDir, tmpDir);
|
|
19803
|
+
await safeReplaceDir(wsContentDir, tmpDir);
|
|
19804
|
+
} catch (err2) {
|
|
19805
|
+
await fs38.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
19806
|
+
});
|
|
19807
|
+
await wsGit.checkout(currentBranch);
|
|
19808
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19809
|
+
});
|
|
19810
|
+
throw err2;
|
|
19811
|
+
}
|
|
19812
|
+
await stageAllExceptCanopyState2(wsGit);
|
|
19813
|
+
const incomingFiles = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
19814
|
+
if (incomingFiles.length === 0) {
|
|
19815
|
+
await wsGit.checkout(currentBranch);
|
|
19816
|
+
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
19817
|
+
p2.log.info("No working-tree changes to merge \u2014 pulling editor changes only");
|
|
19818
|
+
const pullResult2 = await pullFromWorkspace({ ...options, force: true }, ws);
|
|
19819
|
+
return { pushed: 0, pulled: pullResult2.fileCount };
|
|
19820
|
+
}
|
|
19821
|
+
await wsGit.commit("sync: incoming working-tree changes");
|
|
19822
|
+
await wsGit.checkout(currentBranch);
|
|
19823
|
+
p2.log.step("Merging working-tree changes with editor changes...");
|
|
19824
|
+
try {
|
|
19825
|
+
await wsGit.merge([incomingBranch, "--no-edit"]);
|
|
19826
|
+
} catch (mergeError) {
|
|
19827
|
+
let mergeStatus;
|
|
19828
|
+
try {
|
|
19829
|
+
mergeStatus = await wsGit.status();
|
|
19830
|
+
} catch {
|
|
19831
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19394
19832
|
});
|
|
19395
|
-
|
|
19396
|
-
return branches;
|
|
19397
|
-
}
|
|
19398
|
-
const snapshot = {
|
|
19399
|
-
version: REGISTRY_VERSION2,
|
|
19400
|
-
branches,
|
|
19401
|
-
generation: read.token
|
|
19402
|
-
};
|
|
19403
|
-
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
19404
|
-
await fs37.mkdir(this.root, { recursive: true });
|
|
19405
|
-
await fs37.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
19406
|
-
try {
|
|
19407
|
-
await fs37.rename(uniqueTempPath, this.registryPath);
|
|
19408
|
-
} catch (err2) {
|
|
19409
|
-
await fs37.unlink(uniqueTempPath).catch(() => {
|
|
19410
|
-
});
|
|
19411
|
-
throw err2;
|
|
19412
|
-
}
|
|
19413
|
-
return branches;
|
|
19833
|
+
throw mergeError;
|
|
19414
19834
|
}
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19418
|
-
|
|
19419
|
-
*/
|
|
19420
|
-
async scanBranchDirectories() {
|
|
19421
|
-
const branches = [];
|
|
19422
|
-
try {
|
|
19423
|
-
const entries = await fs37.readdir(this.root, { withFileTypes: true });
|
|
19424
|
-
for (const entry of entries) {
|
|
19425
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
19426
|
-
continue;
|
|
19427
|
-
}
|
|
19428
|
-
const branchRoot = path45.join(this.root, entry.name);
|
|
19429
|
-
let meta;
|
|
19430
|
-
try {
|
|
19431
|
-
meta = await readBranchMetadataFile2(branchRoot);
|
|
19432
|
-
} catch (err2) {
|
|
19433
|
-
canopyLogWarn3(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err2)}`);
|
|
19434
|
-
continue;
|
|
19435
|
-
}
|
|
19436
|
-
if (meta) {
|
|
19437
|
-
branches.push({
|
|
19438
|
-
branch: meta.branch,
|
|
19439
|
-
branchRoot,
|
|
19440
|
-
baseRoot: this.root
|
|
19441
|
-
});
|
|
19442
|
-
}
|
|
19443
|
-
}
|
|
19444
|
-
} catch (err2) {
|
|
19445
|
-
if (isNotFoundError(err2)) {
|
|
19446
|
-
return [];
|
|
19447
|
-
}
|
|
19448
|
-
throw err2;
|
|
19835
|
+
if (mergeStatus.conflicted.length > 0) {
|
|
19836
|
+
p2.log.error("Merge conflicts detected in the following files:");
|
|
19837
|
+
for (const file of mergeStatus.conflicted) {
|
|
19838
|
+
p2.log.error(` ${file}`);
|
|
19449
19839
|
}
|
|
19450
|
-
|
|
19840
|
+
p2.log.info("The branch workspace is now in a merge state.");
|
|
19841
|
+
const branchFlag = options.branch ? ` --branch ${options.branch}` : "";
|
|
19842
|
+
p2.log.info(`Resolve the conflicts in the workspace, then run: canopycms sync pull${branchFlag}`);
|
|
19843
|
+
p2.log.info(`Or abort the merge with: canopycms sync abort${branchFlag}`);
|
|
19844
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19845
|
+
});
|
|
19846
|
+
return { pushed: 0, pulled: 0 };
|
|
19847
|
+
}
|
|
19848
|
+
await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
|
|
19849
|
+
});
|
|
19850
|
+
throw mergeError;
|
|
19851
|
+
}
|
|
19852
|
+
await wsGit.raw(["branch", "-D", incomingBranch]);
|
|
19853
|
+
await wsGit.tag(["-f", SYNC_BASE_TAG]);
|
|
19854
|
+
p2.log.success("Merged working-tree changes with editor changes");
|
|
19855
|
+
const pullResult = await pullFromWorkspace({ ...options, force: true }, ws);
|
|
19856
|
+
return { pushed: incomingFiles.length, pulled: pullResult.fileCount };
|
|
19857
|
+
} finally {
|
|
19858
|
+
await invalidateBranchContentCaches2(branchPath);
|
|
19859
|
+
}
|
|
19860
|
+
}
|
|
19861
|
+
async function syncAbort(options) {
|
|
19862
|
+
const ws = await selectBranch({ ...options, projectDir: options.projectDir });
|
|
19863
|
+
if (!ws)
|
|
19864
|
+
return;
|
|
19865
|
+
const { branchName, branchPath } = ws;
|
|
19866
|
+
if (!await abortWorkspaceMerge(branchPath, branchName)) {
|
|
19867
|
+
p2.log.info(`Branch workspace "${branchName}" is not in a merge state \u2014 nothing to abort.`);
|
|
19868
|
+
return;
|
|
19869
|
+
}
|
|
19870
|
+
p2.log.success(`Merge aborted in branch workspace "${branchName}". Workspace restored to pre-merge state.`);
|
|
19871
|
+
}
|
|
19872
|
+
async function sync(options) {
|
|
19873
|
+
p2.intro("CanopyCMS sync");
|
|
19874
|
+
let pushed = 0;
|
|
19875
|
+
let pulled = 0;
|
|
19876
|
+
if (options.direction === "abort") {
|
|
19877
|
+
await syncAbort(options);
|
|
19878
|
+
} else if (options.direction === "both") {
|
|
19879
|
+
const result = await syncBoth(options);
|
|
19880
|
+
pushed = result.pushed;
|
|
19881
|
+
pulled = result.pulled;
|
|
19882
|
+
} else if (options.direction === "push") {
|
|
19883
|
+
const result = await syncPush(options);
|
|
19884
|
+
pushed = result.fileCount;
|
|
19885
|
+
} else {
|
|
19886
|
+
const result = await syncPull(options);
|
|
19887
|
+
pulled = result.fileCount;
|
|
19888
|
+
}
|
|
19889
|
+
p2.outro("Done!");
|
|
19890
|
+
return { pushed, pulled };
|
|
19891
|
+
}
|
|
19892
|
+
var SyncError, branchConfigSchema;
|
|
19893
|
+
var init_sync = __esm({
|
|
19894
|
+
"dist/cli/sync.js"() {
|
|
19895
|
+
"use strict";
|
|
19896
|
+
init_branch_param();
|
|
19897
|
+
init_branch_metadata_file();
|
|
19898
|
+
init_branch();
|
|
19899
|
+
init_validation3();
|
|
19900
|
+
init_error();
|
|
19901
|
+
init_fs();
|
|
19902
|
+
init_git();
|
|
19903
|
+
init_sync_core();
|
|
19904
|
+
init_content_index_generation();
|
|
19905
|
+
init_content_write_lock();
|
|
19906
|
+
init_git_manager();
|
|
19907
|
+
init_operating_mode2();
|
|
19908
|
+
init_site_config();
|
|
19909
|
+
SyncError = class extends Error {
|
|
19910
|
+
constructor(message) {
|
|
19911
|
+
super(message);
|
|
19912
|
+
this.name = "SyncError";
|
|
19451
19913
|
}
|
|
19452
19914
|
};
|
|
19915
|
+
branchConfigSchema = z19.object({
|
|
19916
|
+
branchNamespace: z19.string().optional(),
|
|
19917
|
+
deploymentName: z19.string().optional(),
|
|
19918
|
+
defaultBaseBranch: z19.string().optional(),
|
|
19919
|
+
defaultActiveBranch: z19.string().optional()
|
|
19920
|
+
});
|
|
19453
19921
|
}
|
|
19454
19922
|
});
|
|
19455
19923
|
|
|
19456
|
-
// dist/
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
}
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
createdBy: "unknown",
|
|
19471
|
-
createdAt: now,
|
|
19472
|
-
updatedAt: now
|
|
19473
|
-
};
|
|
19474
|
-
return {
|
|
19475
|
-
// Envelope keys this code does not know survive, as unknown branch keys do below.
|
|
19476
|
-
...existing,
|
|
19477
|
-
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
19478
|
-
version: version ?? 0,
|
|
19479
|
-
branch: {
|
|
19480
|
-
...defaults,
|
|
19481
|
-
...existing?.branch,
|
|
19482
|
-
...incoming.branch,
|
|
19483
|
-
access: {
|
|
19484
|
-
...existing?.branch?.access,
|
|
19485
|
-
...incoming.branch?.access
|
|
19486
|
-
},
|
|
19487
|
-
// Immutable after creation
|
|
19488
|
-
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
19489
|
-
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
19490
|
-
// Fork point is recorded once at creation; later saves must not move it
|
|
19491
|
-
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
19492
|
-
// Always stamped fresh; the spreads above would otherwise let the creation-time value win
|
|
19493
|
-
// forever, freezing the timestamp the editor's Branches panel sorts and displays by
|
|
19494
|
-
updatedAt: now
|
|
19495
|
-
}
|
|
19496
|
-
};
|
|
19924
|
+
// dist/schema/index.js
|
|
19925
|
+
var init_schema = __esm({
|
|
19926
|
+
"dist/schema/index.js"() {
|
|
19927
|
+
"use strict";
|
|
19928
|
+
init_meta_loader();
|
|
19929
|
+
}
|
|
19930
|
+
});
|
|
19931
|
+
|
|
19932
|
+
// dist/schema/schema-store.js
|
|
19933
|
+
import path48 from "node:path";
|
|
19934
|
+
import { z as z20 } from "zod";
|
|
19935
|
+
async function withBranchSchemaLock(branchRoot, fn, contentWriteLockWaitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS2) {
|
|
19936
|
+
const schemaLockPath = path48.join(path48.resolve(branchRoot), ".canopy-meta", "schema");
|
|
19937
|
+
return withContentWriteLock2(branchRoot, () => withLock2(schemaLockPath, () => withOccFileLock2(schemaLockPath, fn)), contentWriteLockWaitMs);
|
|
19497
19938
|
}
|
|
19498
|
-
var
|
|
19499
|
-
var
|
|
19500
|
-
"dist/
|
|
19939
|
+
var MAX_NAME_LENGTH, MAX_LABEL_LENGTH, SAFE_NAME_PATTERN, SAFE_NAME_MESSAGE, entryTypeInputSchema, createCollectionInputSchema, updateCollectionInputSchema, updateEntryTypeInputSchema, log16;
|
|
19940
|
+
var init_schema_store = __esm({
|
|
19941
|
+
"dist/schema/schema-store.js"() {
|
|
19501
19942
|
"use strict";
|
|
19502
|
-
init_branch_registry();
|
|
19503
|
-
init_branch_metadata_file();
|
|
19504
|
-
init_error();
|
|
19505
19943
|
init_async_mutex();
|
|
19944
|
+
init_debug2();
|
|
19945
|
+
init_content_write_lock();
|
|
19506
19946
|
init_occ_json_write();
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
|
|
19512
|
-
}
|
|
19513
|
-
|
|
19514
|
-
|
|
19515
|
-
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
19519
|
-
|
|
19520
|
-
}
|
|
19521
|
-
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
return { meta, version: meta?.version ?? null };
|
|
19541
|
-
}
|
|
19542
|
-
/**
|
|
19543
|
-
* Write branch.json via the shared OCC helper, applying the schemaVersion
|
|
19544
|
-
* default here since payload shaping is branch-metadata's concern.
|
|
19545
|
-
*
|
|
19546
|
-
* Throws the helper's raw {@link OccWriteConflictError}, which is what the
|
|
19547
|
-
* surrounding {@link withOccRetry} in save() recognizes and retries;
|
|
19548
|
-
* translating to the public `BranchMetadataConflictError` earlier than the
|
|
19549
|
-
* save() boundary would make withOccRetry's predicate miss it.
|
|
19550
|
-
*
|
|
19551
|
-
* branch.json is written WITH a trailing newline (`trailingNewline: true`),
|
|
19552
|
-
* unlike comments.json.
|
|
19553
|
-
*/
|
|
19554
|
-
async write(meta, expectedVersion) {
|
|
19555
|
-
const payload = {
|
|
19556
|
-
...meta,
|
|
19557
|
-
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION2
|
|
19558
|
-
};
|
|
19559
|
-
return writeOccJsonFile2(this.filePath, payload, {
|
|
19560
|
-
expectedVersion,
|
|
19561
|
-
settleMs: this.settleMs,
|
|
19562
|
-
trailingNewline: true
|
|
19563
|
-
});
|
|
19564
|
-
}
|
|
19565
|
-
/**
|
|
19566
|
-
* Run a save cycle under the full lock + OCC-retry stack (class doc). A
|
|
19567
|
-
* conflict surviving every retry surfaces as `BranchMetadataConflictError`.
|
|
19568
|
-
*
|
|
19569
|
-
* The stat guards a phantom-resurrection race with branch deletion: a
|
|
19570
|
-
* caller's branchContext can resolve BEFORE a concurrent deleteBranchHandler
|
|
19571
|
-
* removes the branch directory, while the save reaches here after it, and
|
|
19572
|
-
* write()'s `mkdir({recursive:true})` would then recreate `.canopy-meta/` and
|
|
19573
|
-
* branch.json from defaults inside a tree nothing else refers to — a registry
|
|
19574
|
-
* entry with no clone behind it. It runs BEFORE the lock stack so a doomed
|
|
19575
|
-
* save fails fast instead of paying for a lock.
|
|
19576
|
-
*
|
|
19577
|
-
* A save that passes the check just before a delete still cannot write: the
|
|
19578
|
-
* delete renames the tree away under the same lockfile, and that lockfile
|
|
19579
|
-
* creates only its own directory (`withOccFileLock`), so the save fails there.
|
|
19580
|
-
*/
|
|
19581
|
-
async save(incoming) {
|
|
19582
|
-
const saved = await this.saveIf(incoming, () => true);
|
|
19583
|
-
if (saved === null)
|
|
19584
|
-
throw new Error("unreachable: an unconditional save was skipped");
|
|
19585
|
-
return saved;
|
|
19586
|
-
}
|
|
19587
|
-
/**
|
|
19588
|
-
* {@link save}, applied only when `onlyIf` accepts the metadata on disk; resolves null when it
|
|
19589
|
-
* does not, having written nothing. `onlyIf` runs inside the lock stack against the version
|
|
19590
|
-
* the write is checked against, so the decision and the write are one compare-and-set.
|
|
19591
|
-
*/
|
|
19592
|
-
async saveIf(incoming, onlyIf) {
|
|
19593
|
-
return this.update((existing) => onlyIf(existing) ? incoming : null);
|
|
19594
|
-
}
|
|
19595
|
-
/**
|
|
19596
|
-
* A save whose update `compute` derives from the metadata on disk, inside the lock stack, so
|
|
19597
|
-
* an update built from the existing value (adding to a list) cannot lose a concurrent one.
|
|
19598
|
-
* `compute` returning null writes nothing and resolves null. `keepUpdatedAt` leaves
|
|
19599
|
-
* `updatedAt` as it was, for bookkeeping writes that are not a change to the branch.
|
|
19600
|
-
*/
|
|
19601
|
-
async update(compute, options = {}) {
|
|
19602
|
-
try {
|
|
19603
|
-
await fs38.stat(this.branchRoot);
|
|
19604
|
-
} catch (err2) {
|
|
19605
|
-
if (isNotFoundError(err2)) {
|
|
19606
|
-
throw new BranchMetadataConflictError2("Branch no longer exists");
|
|
19607
|
-
}
|
|
19608
|
-
throw err2;
|
|
19609
|
-
}
|
|
19610
|
-
let saved;
|
|
19611
|
-
try {
|
|
19612
|
-
saved = await withLock2(this.filePath, () => withOccFileLock2(this.filePath, () => withOccRetry2(async () => {
|
|
19613
|
-
const { meta: existing, version } = await this.load();
|
|
19614
|
-
const incoming = compute(existing);
|
|
19615
|
-
if (incoming === null)
|
|
19616
|
-
return null;
|
|
19617
|
-
const merged = mergeBranchMetadata2(existing, version, incoming);
|
|
19618
|
-
if (options.keepUpdatedAt && existing)
|
|
19619
|
-
merged.branch.updatedAt = existing.branch.updatedAt;
|
|
19620
|
-
const written = await this.write(merged, version);
|
|
19621
|
-
merged.version = written.version;
|
|
19622
|
-
merged.writeId = written.writeId;
|
|
19623
|
-
return merged;
|
|
19624
|
-
})));
|
|
19625
|
-
} catch (err2) {
|
|
19626
|
-
if (err2 instanceof OccWriteConflictError2) {
|
|
19627
|
-
throw new BranchMetadataConflictError2();
|
|
19628
|
-
}
|
|
19629
|
-
throw err2;
|
|
19630
|
-
}
|
|
19631
|
-
if (saved !== null)
|
|
19632
|
-
await this.invalidateRegistry();
|
|
19633
|
-
return saved;
|
|
19634
|
-
}
|
|
19635
|
-
/**
|
|
19636
|
-
* Adds `userId` to `editors` and `uncommittedEditors`. A lock-free read skips the write when
|
|
19637
|
-
* both already hold it, so only a user's first save since the last submit commit pays for the lock.
|
|
19638
|
-
* Never creates branch.json.
|
|
19639
|
-
*/
|
|
19640
|
-
async recordEditor(userId) {
|
|
19641
|
-
const current = await readBranchMetadataFile2(this.branchRoot);
|
|
19642
|
-
if (!current || isRecordedEditor2(current.branch, userId))
|
|
19643
|
-
return;
|
|
19644
|
-
await this.update((existing) => existing && !isRecordedEditor2(existing.branch, userId) ? {
|
|
19645
|
-
branch: {
|
|
19646
|
-
editors: withMember2(existing.branch.editors, userId),
|
|
19647
|
-
uncommittedEditors: withMember2(existing.branch.uncommittedEditors, userId)
|
|
19648
|
-
}
|
|
19649
|
-
} : null, { keepUpdatedAt: true });
|
|
19650
|
-
}
|
|
19651
|
-
/**
|
|
19652
|
-
* Removes `userIds` from `uncommittedEditors` once a submit commit's trailers name them. Ids
|
|
19653
|
-
* recorded after the commit read them stay, so the next commit names them.
|
|
19654
|
-
*/
|
|
19655
|
-
async markEditorsCommitted(userIds) {
|
|
19656
|
-
const committed = new Set(userIds);
|
|
19657
|
-
if (committed.size === 0)
|
|
19658
|
-
return;
|
|
19659
|
-
await this.update((existing) => {
|
|
19660
|
-
const pending = existing?.branch.uncommittedEditors ?? [];
|
|
19661
|
-
const remaining = pending.filter((id) => !committed.has(id));
|
|
19662
|
-
if (remaining.length === pending.length)
|
|
19663
|
-
return null;
|
|
19664
|
-
return { branch: { uncommittedEditors: remaining.length > 0 ? remaining : void 0 } };
|
|
19665
|
-
}, { keepUpdatedAt: true });
|
|
19666
|
-
}
|
|
19667
|
-
/** Invalidate the registry cache so the next list() regenerates. */
|
|
19668
|
-
async invalidateRegistry() {
|
|
19669
|
-
const registry3 = new BranchRegistry2(this.baseRoot);
|
|
19670
|
-
await registry3.invalidate();
|
|
19671
|
-
}
|
|
19672
|
-
};
|
|
19947
|
+
MAX_NAME_LENGTH = 64;
|
|
19948
|
+
MAX_LABEL_LENGTH = 128;
|
|
19949
|
+
SAFE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
19950
|
+
SAFE_NAME_MESSAGE = "must start with a letter and contain only lowercase letters, numbers, and hyphens";
|
|
19951
|
+
entryTypeInputSchema = z20.object({
|
|
19952
|
+
name: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Entry type name ${SAFE_NAME_MESSAGE}`),
|
|
19953
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19954
|
+
format: z20.enum(["md", "mdx", "json", "yaml"]),
|
|
19955
|
+
schema: z20.string().min(1),
|
|
19956
|
+
default: z20.boolean().optional(),
|
|
19957
|
+
maxItems: z20.number().int().positive().optional()
|
|
19958
|
+
});
|
|
19959
|
+
createCollectionInputSchema = z20.object({
|
|
19960
|
+
name: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`),
|
|
19961
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19962
|
+
parentPath: z20.string().optional(),
|
|
19963
|
+
entries: z20.array(entryTypeInputSchema).min(1, "Collection must have at least one entry type")
|
|
19964
|
+
});
|
|
19965
|
+
updateCollectionInputSchema = z20.object({
|
|
19966
|
+
name: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Collection name ${SAFE_NAME_MESSAGE}`).optional(),
|
|
19967
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19968
|
+
// Directory name (e.g., "posts" in "posts.{id}/")
|
|
19969
|
+
slug: z20.string().min(1).max(MAX_NAME_LENGTH).regex(SAFE_NAME_PATTERN, `Slug ${SAFE_NAME_MESSAGE}`).optional(),
|
|
19970
|
+
order: z20.array(z20.string()).optional()
|
|
19971
|
+
});
|
|
19972
|
+
updateEntryTypeInputSchema = z20.object({
|
|
19973
|
+
label: z20.string().max(MAX_LABEL_LENGTH).optional(),
|
|
19974
|
+
format: z20.enum(["md", "mdx", "json", "yaml"]).optional(),
|
|
19975
|
+
schema: z20.string().min(1).optional(),
|
|
19976
|
+
default: z20.boolean().optional(),
|
|
19977
|
+
maxItems: z20.number().int().positive().optional()
|
|
19978
|
+
});
|
|
19979
|
+
log16 = createDebugLogger3({ prefix: "SchemaOps" });
|
|
19673
19980
|
}
|
|
19674
19981
|
});
|
|
19675
19982
|
|
|
@@ -19681,7 +19988,7 @@ __export(migrate_exports, {
|
|
|
19681
19988
|
slugifyName: () => slugifyName
|
|
19682
19989
|
});
|
|
19683
19990
|
import fs39 from "node:fs/promises";
|
|
19684
|
-
import
|
|
19991
|
+
import path49 from "node:path";
|
|
19685
19992
|
import * as p3 from "@clack/prompts";
|
|
19686
19993
|
function slugifyName(name) {
|
|
19687
19994
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -19715,7 +20022,7 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
19715
20022
|
if (entry.name.startsWith("."))
|
|
19716
20023
|
continue;
|
|
19717
20024
|
if (entry.isDirectory()) {
|
|
19718
|
-
const subPath =
|
|
20025
|
+
const subPath = path49.join(dirPath, entry.name);
|
|
19719
20026
|
const sub = await planDirectory(subPath, false, ctx);
|
|
19720
20027
|
if (!sub.hasContent)
|
|
19721
20028
|
continue;
|
|
@@ -19723,10 +20030,10 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
19723
20030
|
ops.push(...sub.ops);
|
|
19724
20031
|
if (!extractIdFromFilename2(entry.name)) {
|
|
19725
20032
|
const newName = `${toValidSlug(entry.name)}.${generateId2()}`;
|
|
19726
|
-
ops.push({ kind: "rename", from: subPath, to:
|
|
20033
|
+
ops.push({ kind: "rename", from: subPath, to: path49.join(dirPath, newName) });
|
|
19727
20034
|
}
|
|
19728
20035
|
} else if (entry.isFile()) {
|
|
19729
|
-
const ext =
|
|
20036
|
+
const ext = path49.extname(entry.name).slice(1);
|
|
19730
20037
|
const matchesFormat = ext === ctx.format || ctx.format === "yaml" && ext === "yml";
|
|
19731
20038
|
if (!matchesFormat)
|
|
19732
20039
|
continue;
|
|
@@ -19737,17 +20044,17 @@ async function planDirectory(dirPath, isRoot, ctx) {
|
|
|
19737
20044
|
const newName = `${ctx.entryType}.${toValidSlug(base)}.${generateId2()}.${ext}`;
|
|
19738
20045
|
ops.push({
|
|
19739
20046
|
kind: "rename",
|
|
19740
|
-
from:
|
|
19741
|
-
to:
|
|
20047
|
+
from: path49.join(dirPath, entry.name),
|
|
20048
|
+
to: path49.join(dirPath, newName)
|
|
19742
20049
|
});
|
|
19743
20050
|
}
|
|
19744
20051
|
}
|
|
19745
20052
|
}
|
|
19746
20053
|
const needsMeta = isRoot ? hasOwnFiles : hasContent;
|
|
19747
|
-
const metaPath =
|
|
20054
|
+
const metaPath = path49.join(dirPath, ".collection.json");
|
|
19748
20055
|
if (needsMeta && !await filePathExists(metaPath)) {
|
|
19749
20056
|
const entryTypeMeta = { name: ctx.entryType, format: ctx.format, schema: ctx.schema };
|
|
19750
|
-
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(
|
|
20057
|
+
const meta = isRoot ? { entries: [entryTypeMeta] } : { name: toValidSlug(stripDirIdSuffix(path49.basename(dirPath))), entries: [entryTypeMeta] };
|
|
19751
20058
|
ops.push({ kind: "write", filePath: metaPath, content: JSON.stringify(meta, null, 2) + "\n" });
|
|
19752
20059
|
}
|
|
19753
20060
|
return { ops, hasContent };
|
|
@@ -19756,7 +20063,7 @@ async function migrate(options) {
|
|
|
19756
20063
|
const { projectDir } = options;
|
|
19757
20064
|
const contentRoot = options.contentRoot || "content";
|
|
19758
20065
|
p3.intro("CanopyCMS migrate");
|
|
19759
|
-
const contentDir =
|
|
20066
|
+
const contentDir = path49.join(projectDir, contentRoot);
|
|
19760
20067
|
if (!await filePathExists(contentDir)) {
|
|
19761
20068
|
throw new MigrateError(`Content directory not found: ${contentRoot}/ (expected at ${contentDir})`);
|
|
19762
20069
|
}
|
|
@@ -19891,7 +20198,7 @@ var init_migrate = __esm({
|
|
|
19891
20198
|
this.name = "MigrateError";
|
|
19892
20199
|
}
|
|
19893
20200
|
};
|
|
19894
|
-
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${
|
|
20201
|
+
describeOp = (op, baseDir) => op.kind === "rename" ? `rename ${path49.relative(baseDir, op.from)} \u2192 ${path49.basename(op.to)}` : `create ${path49.relative(baseDir, op.filePath)}`;
|
|
19895
20202
|
}
|
|
19896
20203
|
});
|
|
19897
20204
|
|