@hone-ai/cli 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -235,10 +235,72 @@ function parsePlatformConfigPaths(yamlText) {
235
235
  return out;
236
236
  }
237
237
 
238
+ /**
239
+ * Parse adopter-discovered `platform.metadata_types.config[].path` entries
240
+ * from a `.pipeline-config.yml` text. HC-018: closes the classifier loop
241
+ * by feeding setup-time-discovered CONFIG metadata locations into
242
+ * `classifyPathsForRepo` so adopter platforms (Salesforce, NetSuite, dbt,
243
+ * Terraform, ...) classify as CONFIG without needing to be hardcoded
244
+ * into STACK_PATH_TABLE.
245
+ *
246
+ * The YAML shape emitted by cli/lib/config-augment.js is:
247
+ * platform:
248
+ * metadata_types:
249
+ * config:
250
+ * - { type: customObjects, path: "force-app/main/default/objects/", count: 18 }
251
+ *
252
+ * Some discover paths are comma-joined multi-paths
253
+ * (e.g. "force-app/main/default/classes, force-app/main/default/triggers")
254
+ * — those are split into individual entries.
255
+ *
256
+ * Returned values are raw filesystem paths (NOT regex). The caller
257
+ * anchors + escapes them before unioning into `extraConfigPaths`.
258
+ *
259
+ * @param {string} yamlText
260
+ * @returns {string[]} raw path strings discovered under metadata_types.config[]
261
+ */
262
+ function parsePlatformMetadataTypesConfigPaths(yamlText) {
263
+ if (typeof yamlText !== 'string' || yamlText.length === 0) return [];
264
+ const blockMatch = yamlText.match(/^platform:\s*\n((?:[ \t]+.*\n?)*)/m);
265
+ if (!blockMatch) return [];
266
+ const block = blockMatch[1];
267
+ // metadata_types: <body> — body lines indented strictly more than the key
268
+ const mtMatch = block.match(/^([ \t]*)metadata_types:\s*\n((?:\1[ \t]+.*\n?)*)/m);
269
+ if (!mtMatch) return [];
270
+ const mtBody = mtMatch[2];
271
+ // config: <body> under metadata_types — same indent-bounded scan
272
+ const cfgMatch = mtBody.match(/^([ \t]*)config:\s*\n((?:\1[ \t]+.*\n?)*)/m);
273
+ if (!cfgMatch) return [];
274
+ const cfgBody = cfgMatch[2];
275
+ // Match each inline-mapping item: `- { ..., path: "VALUE", ... }`
276
+ // (also accepts single-quoted and bare-token paths)
277
+ const out = [];
278
+ const re = /-\s*\{[^}]*\bpath:\s*(?:"([^"]+)"|'([^']+)'|([^,}\s]+))/g;
279
+ let m;
280
+ while ((m = re.exec(cfgBody)) !== null) {
281
+ const raw = (m[1] || m[2] || m[3] || '').trim();
282
+ if (!raw) continue;
283
+ // Discover code sometimes joins multiple paths with ", " — split them.
284
+ for (const piece of raw.split(/\s*,\s*/)) {
285
+ const v = piece.trim();
286
+ if (v) out.push(v);
287
+ }
288
+ }
289
+ return out;
290
+ }
291
+
292
+ /**
293
+ * Escape a literal filesystem path so it can be used as a RegExp pattern.
294
+ */
295
+ function escapeRegexLiteral(s) {
296
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
297
+ }
298
+
238
299
  /**
239
300
  * Higher-level classifier composing platform fingerprint detection +
240
- * adopter-supplied `.pipeline-config.yml platform.config_paths` + the
241
- * pure `classifyPathsForStack` core.
301
+ * adopter-supplied `.pipeline-config.yml platform.config_paths` +
302
+ * setup-discovered `platform.metadata_types.config[].path` (HC-018) +
303
+ * the pure `classifyPathsForStack` core.
242
304
  *
243
305
  * Pure-helper-with-injected-I/O shape (same as compliance-check.js):
244
306
  * caller wraps `fs.existsSync` and `fs.readFileSync` so the helper can
@@ -249,7 +311,12 @@ function parsePlatformConfigPaths(yamlText) {
249
311
  * @param {string} opts.repoRoot - absolute path to repo root
250
312
  * @param {(relativePath: string) => boolean} [opts.fileExists] - filesystem check
251
313
  * @param {(relativePath: string) => string|null} [opts.readFile] - file reader
252
- * @returns {{ category: string, detectedPlatforms: string[], configPathsUsed: string[] }}
314
+ * @returns {{
315
+ * category: string,
316
+ * detectedPlatforms: string[],
317
+ * configPathsUsed: string[],
318
+ * metadataConfigPathsUsed: string[]
319
+ * }}
253
320
  */
254
321
  function classifyPathsForRepo(opts = {}) {
255
322
  const { paths, repoRoot, fileExists, readFile } = opts;
@@ -261,19 +328,28 @@ function classifyPathsForRepo(opts = {}) {
261
328
  detectedPlatforms = detectPlatforms({ repoRoot, fileExists });
262
329
  } catch { /* defensive — if the module is unavailable, treat as no platforms */ }
263
330
 
264
- // Read adopter-supplied platform.config_paths from .pipeline-config.yml (best-effort)
331
+ // Read adopter-supplied platform.config_paths + discovered metadata_types
332
+ // from .pipeline-config.yml (best-effort)
265
333
  let configPathsUsed = [];
334
+ let metadataConfigPathsUsed = [];
266
335
  try {
267
336
  if (typeof readFile === 'function') {
268
337
  const yamlText = readFile('.pipeline-config.yml');
269
338
  if (typeof yamlText === 'string' && yamlText.length > 0) {
270
339
  configPathsUsed = parsePlatformConfigPaths(yamlText);
340
+ metadataConfigPathsUsed = parsePlatformMetadataTypesConfigPaths(yamlText);
271
341
  }
272
342
  }
273
343
  } catch { /* defensive — bad YAML / missing file is a no-op */ }
274
344
 
275
- const category = classifyPathsForStack(paths, { extraConfigPaths: configPathsUsed });
276
- return { category, detectedPlatforms, configPathsUsed };
345
+ // metadata_types paths are raw filesystem prefixes — anchor + escape
346
+ // so a discovered `force-app/main/default/objects/` is matched as a
347
+ // path prefix, not as a free-form regex.
348
+ const metadataPatterns = metadataConfigPathsUsed.map(p => '^' + escapeRegexLiteral(p));
349
+ const extraConfigPaths = configPathsUsed.concat(metadataPatterns);
350
+
351
+ const category = classifyPathsForStack(paths, { extraConfigPaths });
352
+ return { category, detectedPlatforms, configPathsUsed, metadataConfigPathsUsed };
277
353
  }
278
354
 
279
355
  module.exports = {
@@ -282,4 +358,6 @@ module.exports = {
282
358
  classifyPathsForStack,
283
359
  classifyPathsForRepo,
284
360
  parsePlatformConfigPaths,
361
+ parsePlatformMetadataTypesConfigPaths,
362
+ escapeRegexLiteral,
285
363
  };
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  const { execSync } = require('node:child_process');
22
+ const { gitEnv } = require('./git-env');
22
23
  const fs = require('node:fs');
23
24
  const path = require('node:path');
24
25
 
@@ -181,7 +182,7 @@ function extractRecentlyModifiedOverlap(repoRoot, touchedFiles, windowDays) {
181
182
  try {
182
183
  const out = execSync(
183
184
  `git log --since="${w} days ago" --format=%H -- ${JSON.stringify(rel)}`,
184
- { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
185
+ { cwd: repoRoot, env: gitEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
185
186
  ).trim();
186
187
  if (out) return true;
187
188
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hone-ai/cli",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "Hone AI — Enterprise SDLC Pipeline CLI",
5
5
  "main": "hone-cli.js",
6
6
  "bin": {
@@ -16,6 +16,8 @@
16
16
  "scripts": {
17
17
  "test": "echo \"No tests yet\" && exit 0",
18
18
  "link": "npm link",
19
+ "sync-server-cli-version": "node scripts/sync-server-cli-version.js",
20
+ "prepublishOnly": "node scripts/sync-server-cli-version.js",
19
21
  "postinstall": "echo '\\n Hone AI CLI installed successfully.\\n Next: run `hone init --token <YOUR_TOKEN>` to configure.\\n Docs: https://github.com/subbareddyvani/hone-server\\n'"
20
22
  },
21
23
  "dependencies": {