@redpanda-data/docs-extensions-and-macros 4.13.2 → 4.13.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.
@@ -362,7 +362,7 @@ const tools = [
362
362
  },
363
363
  surface: {
364
364
  type: 'string',
365
- description: 'Which API surface(s) to bundle (optional, defaults to "both")',
365
+ description: 'Which API surfaces to bundle (optional, defaults to "both")',
366
366
  enum: ['admin', 'connect', 'both']
367
367
  },
368
368
  out_admin: {
package/bin/doc-tools.js CHANGED
@@ -1630,7 +1630,7 @@ automation
1630
1630
  .option('-t, --tag <tag>', 'Git tag for released content')
1631
1631
  .option('-b, --branch <branch>', 'Branch name for in-progress content')
1632
1632
  .option('--repo <url>', 'Repository URL', 'https://github.com/redpanda-data/redpanda.git')
1633
- .addOption(new Option('-s, --surface <surface>', 'Which API surface(s) to bundle').choices(['admin', 'connect', 'both']).makeOptionMandatory())
1633
+ .addOption(new Option('-s, --surface <surface>', 'Which API surfaces to bundle').choices(['admin', 'connect', 'both']).makeOptionMandatory())
1634
1634
  .option('--out-admin <path>', 'Output path for admin API', 'admin/redpanda-admin-api.yaml')
1635
1635
  .option('--out-connect <path>', 'Output path for connect API', 'connect/redpanda-connect-api.yaml')
1636
1636
  .option('--admin-major <string>', 'Admin API major version', 'v2.0.0')
@@ -306,10 +306,10 @@ function generateReviewReport(results, outputPath) {
306
306
  // Next steps
307
307
  report += `== Next Steps\n\n`;
308
308
  if (errorIssues.length > 0) {
309
- report += `. *Fix errors first* - Address the ${errorIssues.length} error(s) above\n`;
309
+ report += `. *Fix errors first* - Address the ${errorIssues.length} errors above\n`;
310
310
  }
311
311
  if (warningIssues.length > 0) {
312
- report += `${errorIssues.length > 0 ? '. ' : '. '}*Review warnings* - Prioritize the ${warningIssues.length} warning(s)\n`;
312
+ report += `${errorIssues.length > 0 ? '. ' : '. '}*Review warnings* - Prioritize the ${warningIssues.length} warnings\n`;
313
313
  }
314
314
  const step = errorIssues.length > 0 && warningIssues.length > 0 ? 3 : errorIssues.length > 0 || warningIssues.length > 0 ? 2 : 1;
315
315
  report += `${step > 1 ? '. ' : '. '}*Regenerate documentation* - After making changes, regenerate the docs\n`;
@@ -250,7 +250,7 @@ function formatValidationResults(results, config) {
250
250
  lines.push('✓ Validation passed');
251
251
  } else {
252
252
  lines.push('✗ Validation failed');
253
- lines.push(` ${results.errors.length} error(s) must be fixed`);
253
+ lines.push(` ${results.errors.length} errors must be fixed`);
254
254
  }
255
255
 
256
256
  return lines.join('\n');
@@ -16,7 +16,7 @@ const { getAntoraStructure } = require('./antora');
16
16
  * @param {string} [args.tag] - Git tag for released content (for example, "v24.3.2" or "24.3.2")
17
17
  * @param {string} [args.branch] - Branch name for in-progress content (for example, "dev", "main")
18
18
  * @param {string} [args.repo] - Repository URL
19
- * @param {string} [args.surface] - Which API surface(s) to bundle: 'admin', 'connect', or 'both'
19
+ * @param {string} [args.surface] - Which API surfaces to bundle: 'admin', 'connect', or 'both'
20
20
  * @param {string} [args.out_admin] - Output path for admin API
21
21
  * @param {string} [args.out_connect] - Output path for connect API
22
22
  * @param {string} [args.admin_major] - Admin API major version
@@ -155,7 +155,7 @@ function generateBundleOpenApi(args) {
155
155
  surface,
156
156
  files_generated: filesGenerated,
157
157
  output: output.trim(),
158
- summary: `Bundled OpenAPI documentation for ${surface} API(s) at ${refType} ${version}`
158
+ summary: `Bundled OpenAPI documentation for ${surface} APIs at ${refType} ${version}`
159
159
  };
160
160
  } catch (err) {
161
161
  return {
@@ -73,7 +73,6 @@ module.exports.register = function ({ config }) {
73
73
  const status = attrs.status || extractAttribute(file, 'status')
74
74
  const driverSupport = attrs['driver-support'] || extractAttribute(file, 'driver-support')
75
75
  const cacheSupport = attrs['cache-support'] || extractAttribute(file, 'cache-support')
76
- // FIXED: Use correct attribute name (page-commercial-names, not commercial-names)
77
76
  const commercialNames = attrs['page-commercial-names'] || extractAttribute(file, 'page-commercial-names')
78
77
 
79
78
  const pubUrl = file.pub.url
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.13.2",
3
+ "version": "4.13.3",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -551,7 +551,7 @@ function postProcessBundle(filePath, options, quiet = false) {
551
551
  }
552
552
 
553
553
  /**
554
- * Bundle OpenAPI fragments for the specified API surface(s) from a repository tag and write the resulting bundled YAML files to disk.
554
+ * Bundle OpenAPI fragments for the specified API surfaces from a repository tag and write the resulting bundled YAML files to disk.
555
555
  *
556
556
  * @param {Object} options - Configuration options.
557
557
  * @param {string} options.tag - Git tag to checkout (for example, 'v25.1.1').
@@ -665,13 +665,13 @@ Connectors can have human-friendly commercial names that appear in catalogs and
665
665
  ----
666
666
  = kafka_franz
667
667
  :type: input
668
- :commercial-names: Franz-go, Apache Kafka
668
+ :page-commercial-names: Franz-go, Apache Kafka
669
669
  :categories: ["Services"]
670
670
  ----
671
671
 
672
672
  The `generate-rp-connect-categories` extension reads these commercial names and uses the first one as the primary display name. If multiple names are provided (comma-separated), only the first is used for display.
673
673
 
674
- *Default behavior:* If no `:commercial-names:` attribute is found, the connector key name is used (e.g., `kafka_franz`).
674
+ *Default behavior:* If no `:page-commercial-names:` attribute is found, the connector key name is used (such as `kafka_franz`).
675
675
 
676
676
  *Adding commercial names to new connectors:*
677
677
 
@@ -681,12 +681,12 @@ When generating documentation for new connectors with `--draft-missing`, writers
681
681
  ----
682
682
  ### ✍️ Writer Action Required
683
683
 
684
- For each new connector, please add the `:commercial-names:` attribute to the frontmatter:
684
+ For each new connector, please add the `:page-commercial-names:` attribute to the frontmatter:
685
685
 
686
686
  ```asciidoc
687
687
  = Connector Name
688
688
  :type: input
689
- :commercial-names: Commercial Name, Alternative Name
689
+ :page-commercial-names: Commercial Name, Alternative Name
690
690
  ```
691
691
 
692
692
  _This helps improve discoverability and ensures proper categorization._
@@ -503,7 +503,7 @@ async function analyzeAllBinaries(ossVersion, cloudVersion = null, dataDir = nul
503
503
  // These are connectors that require cgo-enabled builds
504
504
  cgoOnly = findCgoOnlyConnectors(ossIndex, cgoIndex);
505
505
 
506
- console.log(`Done: Cgo analysis complete: ${cgoOnly.length} cgo-only connector(s) found`);
506
+ console.log(`Done: Cgo analysis complete: ${cgoOnly.length} cgo-only connectors found`);
507
507
  if (cgoOnly.length > 0) {
508
508
  console.log(' cgo-only connectors:');
509
509
  cgoOnly.forEach(c => console.log(` • ${c.type}/${c.name} (${c.status})`));
@@ -30,7 +30,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
30
30
 
31
31
  // High-level stats
32
32
  const stats = diffData.summary;
33
- const hasChanges = Object.values(stats).some(v => v > 0);
33
+ const hasChanges = Object.values(stats).some(v => v > 0) || (draftedConnectors && draftedConnectors.length > 0);
34
34
 
35
35
  if (!hasChanges) {
36
36
  lines.push('✅ **No changes detected** - Documentation is up to date');
@@ -91,16 +91,78 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
91
91
  if (stats.newComponents > 0) {
92
92
  lines.push('### ✍️ Writer Action Required');
93
93
  lines.push('');
94
- lines.push('For each new connector, please add the `:commercial-names:` attribute to the frontmatter:');
94
+ lines.push('For each new connector, add the `:page-commercial-names:` attribute to the frontmatter:');
95
95
  lines.push('');
96
96
  lines.push('```asciidoc');
97
97
  lines.push('= Connector Name');
98
98
  lines.push(':type: input');
99
- lines.push(':commercial-names: Commercial Name, Alternative Name');
99
+ lines.push(':page-commercial-names: Commercial Name, Alternative Name');
100
100
  lines.push('```');
101
101
  lines.push('');
102
102
  lines.push('_This helps improve discoverability and ensures proper categorization._');
103
103
  lines.push('');
104
+
105
+ // Check if any new connectors are cloud-supported
106
+ if (binaryAnalysis && binaryAnalysis.comparison) {
107
+ const newConnectorKeys = diffData.details.newComponents.map(c => ({
108
+ key: `${c.type}:${c.name}`,
109
+ type: c.type,
110
+ name: c.name
111
+ }));
112
+
113
+ const cloudSupported = newConnectorKeys.filter(item => {
114
+ // Check both inCloud (OSS+Cloud) and cloudOnly (Cloud-only)
115
+ const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === item.key);
116
+ const cloudOnly = binaryAnalysis.comparison.cloudOnly &&
117
+ binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key);
118
+ return inCloud || cloudOnly;
119
+ });
120
+
121
+ if (cloudSupported.length > 0) {
122
+ lines.push('### ☁️ Cloud Docs Update Required');
123
+ lines.push('');
124
+ lines.push(`**${cloudSupported.length}** new connector${cloudSupported.length !== 1 ? 's are' : ' is'} available in Redpanda Cloud.`);
125
+ lines.push('');
126
+ lines.push('**Action:** Submit a separate PR to https://github.com/redpanda-data/cloud-docs to add the connector pages using include syntax:');
127
+ lines.push('');
128
+
129
+ // Check if any are cloud-only (need partial syntax)
130
+ const cloudOnly = cloudSupported.filter(item =>
131
+ binaryAnalysis.comparison.cloudOnly &&
132
+ binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key)
133
+ );
134
+ const regularCloud = cloudSupported.filter(item =>
135
+ !binaryAnalysis.comparison.cloudOnly ||
136
+ !binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key)
137
+ );
138
+
139
+ if (regularCloud.length > 0) {
140
+ lines.push('**For connectors in pages:**');
141
+ lines.push('```asciidoc');
142
+ lines.push('= Connector Name');
143
+ lines.push('');
144
+ lines.push('include::redpanda-connect:components:page$type/connector-name.adoc[tag=single-source]');
145
+ lines.push('```');
146
+ lines.push('');
147
+ }
148
+
149
+ if (cloudOnly.length > 0) {
150
+ lines.push('**For cloud-only connectors (in partials):**');
151
+ lines.push('```asciidoc');
152
+ lines.push('= Connector Name');
153
+ lines.push('');
154
+ lines.push('include::redpanda-connect:components:partial$components/cloud-only/type/connector-name.adoc[tag=single-source]');
155
+ lines.push('```');
156
+ lines.push('');
157
+ }
158
+
159
+ // Add instruction to update cloud whats-new
160
+ lines.push('**Also update the cloud whats-new file:**');
161
+ lines.push('');
162
+ lines.push('Add entries for the new cloud-supported connectors to the Redpanda Cloud whats-new file in the cloud-docs repository.');
163
+ lines.push('');
164
+ }
165
+ }
104
166
  }
105
167
 
106
168
  // Breaking Changes Section
@@ -250,6 +250,7 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
250
250
  result.binaryAnalysis.details = {
251
251
  cloudSupported: ba.comparison?.inCloud?.map(c => ({ type: c.type, name: c.name, status: c.status })) || [],
252
252
  selfHostedOnly: ba.comparison?.notInCloud?.map(c => ({ type: c.type, name: c.name, status: c.status })) || [],
253
+ cloudOnly: ba.comparison?.cloudOnly?.map(c => ({ type: c.type, name: c.name, status: c.status })) || [],
253
254
  cgoOnly: ba.cgoOnly?.map(c => ({ type: c.type, name: c.name, status: c.status })) || []
254
255
  };
255
256
  }
@@ -148,13 +148,20 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
148
148
  const regularComponents = []
149
149
 
150
150
  if (diff.details.newComponents && diff.details.newComponents.length) {
151
- for (const comp of diff.details.newComponents) {
151
+ // Filter out cloud-only connectors - they don't go in whats-new.adoc
152
+ const nonCloudOnlyComponents = diff.details.newComponents.filter(comp => {
153
+ const isCloudOnly = diff.binaryAnalysis?.details?.cloudOnly?.some(cloudComp => {
154
+ return cloudComp.name === comp.name && cloudComp.type === comp.type
155
+ })
156
+ return !isCloudOnly
157
+ })
158
+
159
+ for (const comp of nonCloudOnlyComponents) {
152
160
  if (comp.type === 'bloblang-functions' || comp.type === 'bloblang-methods') {
153
161
  bloblangComponents.push(comp)
154
162
  } else {
155
- const isCgoOnly = binaryAnalysis?.cgoOnly?.some(cgo => {
156
- const typeSingular = cgo.type.replace(/s$/, '')
157
- return cgo.name === comp.name && typeSingular === comp.type
163
+ const isCgoOnly = diff.binaryAnalysis?.details?.cgoOnly?.some(cgo => {
164
+ return cgo.name === comp.name && cgo.type === comp.type
158
165
  })
159
166
 
160
167
  regularComponents.push({
@@ -654,6 +661,27 @@ async function handleRpcnConnectorDocs (options) {
654
661
 
655
662
  let newIndex = JSON.parse(fs.readFileSync(dataFile, 'utf8'))
656
663
 
664
+ // Save a clean copy of OSS data for binary analysis (before augmentation)
665
+ // This ensures the binary analyzer compares actual binaries, not augmented data
666
+ const cleanOssDataPath = path.join(dataDir, `._connect-${newVersion}-clean.json`)
667
+
668
+ // Strip augmentation fields to create clean data for comparison
669
+ const cleanData = JSON.parse(JSON.stringify(newIndex))
670
+ const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits', 'buffers', 'metrics', 'scanners', 'tracers']
671
+
672
+ for (const type of connectorTypes) {
673
+ if (Array.isArray(cleanData[type])) {
674
+ cleanData[type] = cleanData[type].filter(c => !c.cloudOnly) // Remove cloud-only connectors added by augmentation
675
+ cleanData[type].forEach(c => {
676
+ delete c.cloudSupported
677
+ delete c.requiresCgo
678
+ delete c.cloudOnly
679
+ })
680
+ }
681
+ }
682
+
683
+ fs.writeFileSync(cleanOssDataPath, JSON.stringify(cleanData, null, 2), 'utf8')
684
+
657
685
  const versionsMatch = oldVersion && newVersion && oldVersion === newVersion
658
686
  if (versionsMatch) {
659
687
  console.log(`\n✓ Already at version ${newVersion}`)
@@ -740,6 +768,19 @@ async function handleRpcnConnectorDocs (options) {
740
768
  console.log('\nAnalyzing connector binaries...')
741
769
  const { analyzeAllBinaries } = require('./connector-binary-analyzer.js')
742
770
 
771
+ // Always use clean OSS data for comparison
772
+ // Temporarily rename the file so the analyzer finds it
773
+ const expectedPath = path.join(dataDir, `connect-${newVersion}.json`)
774
+ let tempRenamed = false
775
+
776
+ if (fs.existsSync(cleanOssDataPath)) {
777
+ if (fs.existsSync(expectedPath)) {
778
+ fs.renameSync(expectedPath, path.join(dataDir, `._connect-${newVersion}-augmented.json.tmp`))
779
+ tempRenamed = true
780
+ }
781
+ fs.copyFileSync(cleanOssDataPath, expectedPath)
782
+ }
783
+
743
784
  const analysisOptions = {
744
785
  skipCloud: false,
745
786
  skipCgo: false,
@@ -753,6 +794,13 @@ async function handleRpcnConnectorDocs (options) {
753
794
  analysisOptions
754
795
  )
755
796
 
797
+ // Restore the augmented file
798
+ if (tempRenamed) {
799
+ const expectedPath = path.join(dataDir, `connect-${newVersion}.json`)
800
+ fs.unlinkSync(expectedPath)
801
+ fs.renameSync(path.join(dataDir, `._connect-${newVersion}-augmented.json.tmp`), expectedPath)
802
+ }
803
+
756
804
  console.log('Done: Binary analysis complete:')
757
805
  console.log(` • OSS version: ${binaryAnalysis.ossVersion}`)
758
806
 
@@ -848,10 +896,10 @@ async function handleRpcnConnectorDocs (options) {
848
896
  fs.writeFileSync(dataFile, JSON.stringify(connectorData, null, 2), 'utf8')
849
897
  console.log(`Done: Augmented ${augmentedCount} connectors with cloud/cgo fields`)
850
898
  if (addedCgoCount > 0) {
851
- console.log(` • Added ${addedCgoCount} cgo-only connector(s) to data file`)
899
+ console.log(` • Added ${addedCgoCount} cgo-only connectors to data file`)
852
900
  }
853
901
  if (addedCloudOnlyCount > 0) {
854
- console.log(` • Added ${addedCloudOnlyCount} cloud-only connector(s) to data file`)
902
+ console.log(` • Added ${addedCloudOnlyCount} cloud-only connectors to data file`)
855
903
  }
856
904
 
857
905
  // Keep only 2 most recent versions
@@ -890,8 +938,42 @@ async function handleRpcnConnectorDocs (options) {
890
938
  }
891
939
  )
892
940
 
941
+ // Filter out components that already have documentation
942
+ const docRoots = {
943
+ pages: path.resolve(process.cwd(), 'modules/components/pages'),
944
+ partials: path.resolve(process.cwd(), 'modules/components/partials/components'),
945
+ cloudOnly: path.resolve(process.cwd(), 'modules/components/partials/components/cloud-only')
946
+ }
947
+
948
+ if (diffJson.details && diffJson.details.newComponents) {
949
+ const originalCount = diffJson.details.newComponents.length
950
+ diffJson.details.newComponents = diffJson.details.newComponents.filter(comp => {
951
+ const typePlural = comp.type.endsWith('s') ? comp.type : `${comp.type}s`
952
+ const relPath = path.join(typePlural, `${comp.name}.adoc`)
953
+ const docsExist = Object.values(docRoots).some(root =>
954
+ fs.existsSync(path.join(root, relPath))
955
+ )
956
+ return !docsExist
957
+ })
958
+ const filteredCount = originalCount - diffJson.details.newComponents.length
959
+ if (filteredCount > 0) {
960
+ console.log(` ℹ️ Filtered out ${filteredCount} components that already have documentation`)
961
+ }
962
+ // Update summary count
963
+ if (diffJson.summary) {
964
+ diffJson.summary.newComponents = diffJson.details.newComponents.length
965
+ }
966
+ }
967
+
893
968
  // Add new cgo-only components
894
969
  if (binaryAnalysis && binaryAnalysis.cgoOnly && binaryAnalysis.cgoOnly.length > 0) {
970
+ // Define roots for checking if docs already exist
971
+ const docRoots = {
972
+ pages: path.resolve(process.cwd(), 'modules/components/pages'),
973
+ partials: path.resolve(process.cwd(), 'modules/components/partials/components'),
974
+ cloudOnly: path.resolve(process.cwd(), 'modules/components/partials/components/cloud-only')
975
+ }
976
+
895
977
  let newCgoComponents
896
978
 
897
979
  if (oldBinaryAnalysis) {
@@ -899,20 +981,36 @@ async function handleRpcnConnectorDocs (options) {
899
981
  newCgoComponents = binaryAnalysis.cgoOnly.filter(cgoComp => {
900
982
  const wasInOldOss = oldIndex[cgoComp.type]?.some(c => c.name === cgoComp.name)
901
983
  const wasInOldCgo = oldCgoSet.has(`${cgoComp.type}:${cgoComp.name}`)
902
- return !wasInOldOss && !wasInOldCgo
984
+
985
+ // Check if docs already exist
986
+ const typePlural = cgoComp.type.endsWith('s') ? cgoComp.type : `${cgoComp.type}s`
987
+ const relPath = path.join(typePlural, `${cgoComp.name}.adoc`)
988
+ const docsExist = Object.values(docRoots).some(root =>
989
+ fs.existsSync(path.join(root, relPath))
990
+ )
991
+
992
+ return !wasInOldOss && !wasInOldCgo && !docsExist
903
993
  })
904
994
  } else {
905
995
  newCgoComponents = binaryAnalysis.cgoOnly.filter(cgoComp => {
906
996
  const wasInOldOss = oldIndex[cgoComp.type]?.some(c => c.name === cgoComp.name)
907
- return !wasInOldOss
997
+
998
+ // Check if docs already exist
999
+ const typePlural = cgoComp.type.endsWith('s') ? cgoComp.type : `${cgoComp.type}s`
1000
+ const relPath = path.join(typePlural, `${cgoComp.name}.adoc`)
1001
+ const docsExist = Object.values(docRoots).some(root =>
1002
+ fs.existsSync(path.join(root, relPath))
1003
+ )
1004
+
1005
+ return !wasInOldOss && !docsExist
908
1006
  })
909
1007
  if (newCgoComponents.length > 0) {
910
- console.log(` ℹ️ No old binary analysis found - treating ${newCgoComponents.length} cgo component(s) not in old OSS data as new`)
1008
+ console.log(` ℹ️ No old binary analysis found - treating ${newCgoComponents.length} cgo components not in old OSS data as new`)
911
1009
  }
912
1010
  }
913
1011
 
914
1012
  if (newCgoComponents && newCgoComponents.length > 0) {
915
- console.log(` • Found ${newCgoComponents.length} new cgo-only component(s)`)
1013
+ console.log(` • Found ${newCgoComponents.length} new cgo-only components`)
916
1014
  newCgoComponents.forEach(cgoComp => {
917
1015
  const typeSingular = cgoComp.type.replace(/s$/, '')
918
1016
  diffJson.details.newComponents.push({
@@ -940,7 +1038,7 @@ async function handleRpcnConnectorDocs (options) {
940
1038
  .filter(f => f.startsWith('connect-diff-') && f.endsWith('.json') && f !== path.basename(diffPath))
941
1039
 
942
1040
  if (oldDiffFiles.length > 0) {
943
- console.log(`🧹 Cleaning up ${oldDiffFiles.length} old diff file(s)...`)
1041
+ console.log(`🧹 Cleaning up ${oldDiffFiles.length} old diff files...`)
944
1042
  oldDiffFiles.forEach(f => {
945
1043
  const oldDiffPath = path.join(dataDir, f)
946
1044
  fs.unlinkSync(oldDiffPath)
@@ -1011,7 +1109,8 @@ async function handleRpcnConnectorDocs (options) {
1011
1109
 
1012
1110
  const roots = {
1013
1111
  pages: path.resolve(process.cwd(), 'modules/components/pages'),
1014
- partials: path.resolve(process.cwd(), 'modules/components/partials/components')
1112
+ partials: path.resolve(process.cwd(), 'modules/components/partials/components'),
1113
+ cloudOnly: path.resolve(process.cwd(), 'modules/components/partials/components/cloud-only')
1015
1114
  }
1016
1115
 
1017
1116
  const allMissing = validConnectors.filter(({ name, type }) => {
@@ -190,7 +190,18 @@ function updateNavFromDrafts(draftFiles, navPath = null) {
190
190
 
191
191
  console.log(`📝 Updating navigation: ${navPath}`);
192
192
 
193
- const connectors = draftFiles.map(draft => ({
193
+ // Filter out cloud-only connectors (they go in partials, not nav)
194
+ const nonCloudOnlyFiles = draftFiles.filter(draft => !draft.cloudOnly);
195
+ const cloudOnlyFiles = draftFiles.filter(draft => draft.cloudOnly);
196
+
197
+ if (cloudOnlyFiles.length > 0) {
198
+ console.log(` ℹ️ Skipping ${cloudOnlyFiles.length} cloud-only connectors (partials don't need nav entries):`);
199
+ cloudOnlyFiles.forEach(draft => {
200
+ console.log(` • ${draft.type}/${draft.name}`);
201
+ });
202
+ }
203
+
204
+ const connectors = nonCloudOnlyFiles.map(draft => ({
194
205
  type: draft.type,
195
206
  name: draft.name
196
207
  }));