@aifabrix/builder 2.44.6 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.cursor/rules/cli-layout.mdc +7 -3
  2. package/jest.projects.js +56 -0
  3. package/lib/app/helpers.js +3 -3
  4. package/lib/app/index.js +3 -3
  5. package/lib/app/register.js +7 -6
  6. package/lib/app/restart-display.js +52 -21
  7. package/lib/app/rotate-secret.js +7 -6
  8. package/lib/app/run-helpers.js +15 -8
  9. package/lib/app/run.js +57 -9
  10. package/lib/app/show-display.js +7 -0
  11. package/lib/app/show.js +87 -5
  12. package/lib/build/index.js +9 -5
  13. package/lib/cli/infra-guided.js +42 -27
  14. package/lib/cli/installation-log-command.js +73 -0
  15. package/lib/cli/setup-app.js +11 -1
  16. package/lib/cli/setup-auth.js +94 -49
  17. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  18. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  19. package/lib/cli/setup-infra.js +60 -119
  20. package/lib/cli/setup-platform.js +1 -1
  21. package/lib/cli/setup-utility-resolve.js +132 -0
  22. package/lib/cli/setup-utility.js +65 -51
  23. package/lib/commands/app-logs.js +81 -33
  24. package/lib/commands/auth-config.js +116 -18
  25. package/lib/commands/setup-modes.js +19 -6
  26. package/lib/commands/setup-prompts.js +41 -8
  27. package/lib/commands/setup.js +114 -9
  28. package/lib/commands/teardown.js +54 -5
  29. package/lib/commands/up-common.js +48 -14
  30. package/lib/commands/up-dataplane.js +21 -18
  31. package/lib/commands/up-miso.js +12 -8
  32. package/lib/commands/upload.js +5 -3
  33. package/lib/core/audit-logger.js +1 -34
  34. package/lib/core/config-admin-email.js +56 -0
  35. package/lib/core/config-normalize.js +60 -0
  36. package/lib/core/config-registered-controller-urls.js +54 -0
  37. package/lib/core/config.js +33 -50
  38. package/lib/core/secrets-ensure-infra.js +1 -1
  39. package/lib/core/secrets-env-content.js +86 -90
  40. package/lib/core/secrets-env-declarative-expand.js +170 -0
  41. package/lib/core/secrets-env-write.js +2 -0
  42. package/lib/core/secrets-load.js +106 -102
  43. package/lib/external-system/deploy.js +5 -1
  44. package/lib/internal/node-fs.js +2 -0
  45. package/lib/schema/application-schema.json +4 -0
  46. package/lib/schema/infra.parameter.yaml +10 -0
  47. package/lib/utils/app-config-resolver.js +24 -1
  48. package/lib/utils/applications-config-defaults.js +206 -0
  49. package/lib/utils/auth-config-validator.js +2 -12
  50. package/lib/utils/bash-secret-env.js +1 -1
  51. package/lib/utils/compose-generate-docker-compose.js +111 -6
  52. package/lib/utils/compose-generator.js +17 -8
  53. package/lib/utils/controller-url.js +50 -7
  54. package/lib/utils/env-copy.js +99 -14
  55. package/lib/utils/env-template.js +5 -1
  56. package/lib/utils/health-check-url.js +18 -15
  57. package/lib/utils/health-check.js +7 -5
  58. package/lib/utils/infra-optional-service-flags.js +69 -0
  59. package/lib/utils/installation-log-core.js +282 -0
  60. package/lib/utils/installation-log-record.js +237 -0
  61. package/lib/utils/installation-log.js +123 -0
  62. package/lib/utils/log-redaction.js +105 -0
  63. package/lib/utils/manifest-location.js +164 -0
  64. package/lib/utils/manifest-source-emit.js +162 -0
  65. package/lib/utils/paths.js +238 -89
  66. package/lib/utils/remote-secrets-loader.js +7 -1
  67. package/lib/utils/run-cli-flags.js +29 -0
  68. package/lib/utils/secrets-canonical.js +10 -3
  69. package/lib/utils/secrets-path.js +3 -4
  70. package/lib/utils/secrets-utils.js +20 -10
  71. package/lib/utils/system-builder-root.js +10 -2
  72. package/lib/utils/url-declarative-public-base.js +80 -12
  73. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  74. package/lib/utils/url-declarative-resolve-build.js +24 -393
  75. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  76. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  77. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  78. package/lib/utils/url-declarative-resolve.js +47 -7
  79. package/lib/utils/url-declarative-runtime-base-path.js +21 -1
  80. package/lib/utils/urls-local-registry-scan.js +103 -0
  81. package/lib/utils/urls-local-registry.js +161 -90
  82. package/package.json +3 -1
  83. package/templates/applications/dataplane/application.yaml +4 -0
  84. package/templates/applications/miso-controller/application.yaml +2 -0
  85. package/templates/applications/miso-controller/env.template +27 -29
  86. package/.npmrc.token +0 -1
@@ -44,13 +44,15 @@ function getConfigDirForPaths() {
44
44
  }
45
45
 
46
46
  /**
47
- * User-owned `secrets.local.yaml` under {@link getAifabrixHome} (same rule as `aifabrix-home` in
48
- * `config.yaml`, else `~/.aifabrix`). Keeps `secret set` / merge loaders aligned with declared home.
47
+ * User-owned `secrets.local.yaml` beside `config.yaml` (same directory as {@link getConfigDirForPaths}).
48
+ * When `aifabrix-home` is the POSIX home (e.g. `/home/user`), secrets stay under `~/.aifabrix/`, not
49
+ * in the home root. {@link getAifabrixHome} remains for applications base, builder parent, and
50
+ * legacy secrets migration reads in {@link module:lib/utils/secrets-utils}.
49
51
  *
50
52
  * @returns {string} Absolute path to secrets.local.yaml
51
53
  */
52
54
  function getPrimaryUserSecretsLocalPath() {
53
- return path.join(getAifabrixHome(), 'secrets.local.yaml');
55
+ return path.join(getConfigDirForPaths(), 'secrets.local.yaml');
54
56
  }
55
57
 
56
58
  /**
@@ -315,7 +317,8 @@ function getDevDirectory(appName, developerId) {
315
317
 
316
318
  /**
317
319
  * Gets the application path (builder or integration folder).
318
- * Matches getBuilderPath / getIntegrationPath: respects AIFABRIX_BUILDER_DIR and project-root vs cwd base.
320
+ * Matches {@link getBuilderPath} / {@link getIntegrationPath} (cwd `integration/` / `builder/`, then
321
+ * material `(aifabrix-work | aifabrix-home)` trees).
319
322
  * @param {string} appName - Application name
320
323
  * @param {string} [appType] - Application type ('external' or other)
321
324
  * @returns {string} Absolute path to application directory
@@ -325,65 +328,99 @@ function getAppPath(appName, appType) {
325
328
  throw new Error('App name is required and must be a string');
326
329
  }
327
330
  if (appType === 'external') {
328
- return getIntegrationPath(appName);
331
+ return module.exports.getIntegrationPath(appName);
329
332
  }
330
- return getBuilderPath(appName);
333
+ return module.exports.getBuilderPath(appName);
331
334
  }
332
335
 
333
336
  /**
334
- * Base directory for integration/builder: project root when cwd is inside project, else cwd.
335
- * When `aifabrix-work` / `AIFABRIX_WORK` points at a repo that contains `integration/`, use that
336
- * root even if cwd is elsewhere (e.g. global CLI install + cwd under `integration/<app>/`).
337
- * @returns {string} Directory to resolve integration/ and builder/ from
337
+ * Apps materialization / default repo root: **`aifabrix-work`** (or `AIFABRIX_WORK`) when set, else
338
+ * {@link getAifabrixHome}. Used for `integration/` and `builder/` under that parent (not the CLI
339
+ * install tree). Aligns with setup / `up-platform` / `up-miso` / `up-dataplane` template targets.
340
+ *
341
+ * @returns {string} Absolute directory (no trailing `integration/` or `builder/`)
338
342
  */
339
- function getIntegrationBuilderBaseDir() {
343
+ function getAppsMaterializationParent() {
340
344
  const work = getAifabrixWork();
341
345
  if (work) {
342
- const workNorm = path.resolve(work);
343
- const integrationUnderWork = path.join(workNorm, 'integration');
344
- try {
345
- if (nodeFs().existsSync(integrationUnderWork)) {
346
- return workNorm;
347
- }
348
- } catch {
349
- // ignore fs errors
350
- }
346
+ return path.resolve(work);
351
347
  }
352
- const root = getProjectRoot();
353
- const cwd = path.resolve(process.cwd());
354
- const rootNorm = path.resolve(root);
355
- if (cwd === rootNorm || cwd.startsWith(rootNorm + path.sep)) {
356
- return rootNorm;
357
- }
358
- return cwd;
348
+ return path.resolve(getAifabrixHome());
349
+ }
350
+
351
+ /**
352
+ * Base directory for legacy callers that meant “non-cwd app tree”: same as {@link getAppsMaterializationParent}.
353
+ *
354
+ * @returns {string}
355
+ */
356
+ function getIntegrationBuilderBaseDir() {
357
+ return getAppsMaterializationParent();
359
358
  }
360
359
 
361
360
  /**
362
- * Returns the integration root directory (used for listing apps).
361
+ * Returns the default integration root under {@link getAppsMaterializationParent} (listing may also
362
+ * scan {@link getCwdIntegrationRoot}; see {@link listIntegrationAppNames}).
363
+ *
363
364
  * @returns {string} Absolute path to integration/ directory
364
365
  */
365
366
  function getIntegrationRoot() {
366
- return path.join(getIntegrationBuilderBaseDir(), 'integration');
367
+ return path.join(getAppsMaterializationParent(), 'integration');
367
368
  }
368
369
 
369
370
  /**
370
- * Returns the builder root directory. Uses AIFABRIX_BUILDER_DIR when set, else project/cwd + builder.
371
+ * Absolute `integration/` next to {@link process.cwd} when that directory exists.
372
+ *
373
+ * @returns {string|null}
374
+ */
375
+ function getCwdIntegrationRoot() {
376
+ const p = path.join(path.resolve(process.cwd()), 'integration');
377
+ try {
378
+ if (nodeFs().existsSync(p)) {
379
+ const st = nodeFs().statSync(p);
380
+ if (st && typeof st.isDirectory === 'function' && st.isDirectory()) {
381
+ return p;
382
+ }
383
+ }
384
+ } catch {
385
+ // ignore
386
+ }
387
+ return null;
388
+ }
389
+
390
+ /**
391
+ * Absolute `builder/` next to {@link process.cwd} when that directory exists.
392
+ *
393
+ * @returns {string|null}
394
+ */
395
+ function getCwdBuilderRoot() {
396
+ const p = path.join(path.resolve(process.cwd()), 'builder');
397
+ try {
398
+ if (nodeFs().existsSync(p)) {
399
+ const st = nodeFs().statSync(p);
400
+ if (st && typeof st.isDirectory === 'function' && st.isDirectory()) {
401
+ return p;
402
+ }
403
+ }
404
+ } catch {
405
+ // ignore
406
+ }
407
+ return null;
408
+ }
409
+
410
+ /**
411
+ * Returns the material `builder/` root: {@link getAppsMaterializationParent}`/builder`
412
+ * (same as {@link getSystemBuilderRoot}). Does not use `AIFABRIX_BUILDER_DIR` — app paths follow
413
+ * cwd + `integration/` / `builder/` or this root only.
414
+ *
371
415
  * @returns {string} Absolute path to builder/ directory
372
416
  */
373
417
  function getBuilderRoot() {
374
- const envDir = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
375
- ? process.env.AIFABRIX_BUILDER_DIR.trim()
376
- : null;
377
- if (envDir) {
378
- return path.resolve(envDir);
379
- }
380
- return path.join(getIntegrationBuilderBaseDir(), 'builder');
418
+ return path.join(getAppsMaterializationParent(), 'builder');
381
419
  }
382
420
 
383
421
  /**
384
- * Platform system apps: project `builder/<app>` when present; else `builder/<app>` under the
385
- * resolved system-builder parent (config dir, or `aifabrix-home` when outside config — see
386
- * {@link getSystemBuilderRoot}).
422
+ * Platform system apps (`up-platform` / `up-miso` / `up-dataplane` / `setup`): materialize under
423
+ * {@link getSystemBuilderRoot} (= {@link getAppsMaterializationParent}`/builder`), never the global CLI package tree.
387
424
  * @readonly
388
425
  */
389
426
  const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane']);
@@ -398,7 +435,8 @@ function isSystemBuilderAppName(appName) {
398
435
  }
399
436
 
400
437
  /**
401
- * Project/cwd `builder/<appName>` (no existence check).
438
+ * `builder/<appName>` under {@link getAppsMaterializationParent} (same tree as {@link getSystemBuilderRoot}).
439
+ *
402
440
  * @param {string} appName
403
441
  * @returns {string}
404
442
  */
@@ -407,20 +445,30 @@ function getProjectBuilderAppPath(appName) {
407
445
  }
408
446
 
409
447
  /**
410
- * Directory containing default materialization for platform apps: `<parent>/builder/<app>`.
411
- * Parent is {@link getAifabrixSystemDir} when that path lies under {@link getAifabrixHome};
412
- * otherwise {@link getAifabrixHome} (honours `aifabrix-home` / `AIFABRIX_HOME` vs config location).
448
+ * Same parent as {@link getAppsMaterializationParent} (exported for diagnostics / allowlist checks).
449
+ *
450
+ * @returns {string} Absolute path (no trailing `builder/`)
451
+ */
452
+ function getSystemPlatformMaterializationParent() {
453
+ return getAppsMaterializationParent();
454
+ }
455
+
456
+ /**
457
+ * Default `builder/` root for materialized platform apps and Tier‑2 manifest discovery:
458
+ * {@link getAppsMaterializationParent}`/builder`.
413
459
  *
414
460
  * @returns {string}
415
461
  */
416
462
  function getSystemBuilderRoot() {
417
- return path.join(
418
- resolveSystemBuilderParentDir(getAifabrixSystemDir(), getAifabrixHome()),
419
- 'builder'
420
- );
463
+ return path.join(getAppsMaterializationParent(), 'builder');
421
464
  }
422
465
 
423
466
  /**
467
+ * True when `projectAppPath` is a directory that contains a resolvable application config
468
+ * (`application.yaml` / `.json` / legacy `variables.yaml`). Empty `builder/<platformApp>`
469
+ * stubs must not win over {@link getSystemBuilderRoot} or secrets/run would read the wrong tree
470
+ * (missing `env.template` → skipped ensure → "Missing secrets" on first platform boot).
471
+ *
424
472
  * @param {string} projectAppPath - Absolute `.../builder/<app>`
425
473
  * @returns {boolean}
426
474
  */
@@ -428,7 +476,10 @@ function isProjectBuilderAppDirectory(projectAppPath) {
428
476
  try {
429
477
  if (!projectAppPath || !nodeFs().existsSync(projectAppPath)) return false;
430
478
  const st = nodeFs().statSync(projectAppPath);
431
- return Boolean(st && typeof st.isDirectory === 'function' && st.isDirectory());
479
+ if (!st || typeof st.isDirectory !== 'function' || !st.isDirectory()) return false;
480
+ const { resolveApplicationConfigPath } = require('./app-config-resolver');
481
+ resolveApplicationConfigPath(projectAppPath);
482
+ return true;
432
483
  } catch {
433
484
  return false;
434
485
  }
@@ -458,21 +509,14 @@ function isAppSubdirSync(root, name) {
458
509
  */
459
510
  function listIntegrationAppNames() {
460
511
  const disk = nodeFs();
461
- const root = getIntegrationRoot();
462
- if (!disk.existsSync(root)) {
463
- return [];
464
- }
465
- let rootStat;
466
- try {
467
- rootStat = disk.statSync(root);
468
- } catch {
469
- return [];
470
- }
471
- if (!rootStat || typeof rootStat.isDirectory !== 'function' || !rootStat.isDirectory()) {
472
- return [];
512
+ const names = new Set();
513
+ const cwdRoot = getCwdIntegrationRoot();
514
+ if (cwdRoot) {
515
+ addBuilderSubdirNamesToSet(disk, cwdRoot, names, null);
473
516
  }
474
- const entries = disk.readdirSync(root);
475
- return entries.filter((name) => isAppSubdirSync(root, name)).sort();
517
+ const matInt = getIntegrationRoot();
518
+ addBuilderSubdirNamesToSet(disk, matInt, names, null);
519
+ return [...names].sort();
476
520
  }
477
521
 
478
522
  /**
@@ -503,21 +547,29 @@ function addBuilderSubdirNamesToSet(disk, builderRootDir, names, nameFilter) {
503
547
  }
504
548
 
505
549
  /**
506
- * Lists app names (directories) under builder root. Excludes dot-prefixed entries.
507
- * Merges project `builder/` with system builder root (platform keys only under the latter).
550
+ * Lists app names (directories) under builder roots. Excludes dot-prefixed entries.
551
+ * Merges `cwd/builder` and material `(aifabrix-work | aifabrix-home)/builder` (deduped).
508
552
  * @returns {string[]} Sorted list of app directory names
509
553
  */
510
554
  function listBuilderAppNames() {
511
555
  const disk = nodeFs();
512
556
  const names = new Set();
513
- const projectBuilder = path.join(getIntegrationBuilderBaseDir(), 'builder');
514
- addBuilderSubdirNamesToSet(disk, projectBuilder, names, null);
515
- addBuilderSubdirNamesToSet(disk, getSystemBuilderRoot(), names, isSystemBuilderAppName);
557
+ const roots = new Set();
558
+ const cwdRoot = getCwdBuilderRoot();
559
+ if (cwdRoot) {
560
+ roots.add(path.resolve(cwdRoot));
561
+ }
562
+ roots.add(path.resolve(getSystemBuilderRoot()));
563
+ for (const r of roots) {
564
+ addBuilderSubdirNamesToSet(disk, r, names, null);
565
+ }
516
566
  return [...names].sort();
517
567
  }
518
568
 
519
569
  /**
520
- * Gets the integration folder path for external systems.
570
+ * Gets the integration folder path: **`cwd/integration/<appName>`** when that directory exists, else
571
+ * **`aifabrix-work` or `aifabrix-home` + `/integration/<appName>`**.
572
+ *
521
573
  * @param {string} appName - Application name
522
574
  * @returns {string} Absolute path to integration directory
523
575
  */
@@ -525,8 +577,18 @@ function getIntegrationPath(appName) {
525
577
  if (!appName || typeof appName !== 'string') {
526
578
  throw new Error('App name is required and must be a string');
527
579
  }
528
- const base = getIntegrationBuilderBaseDir();
529
- return path.join(base, 'integration', appName);
580
+ const cwdInt = path.join(path.resolve(process.cwd()), 'integration', appName);
581
+ try {
582
+ if (nodeFs().existsSync(cwdInt)) {
583
+ const st = nodeFs().statSync(cwdInt);
584
+ if (st && typeof st.isDirectory === 'function' && st.isDirectory()) {
585
+ return cwdInt;
586
+ }
587
+ }
588
+ } catch {
589
+ // ignore
590
+ }
591
+ return path.join(getAppsMaterializationParent(), 'integration', appName);
530
592
  }
531
593
 
532
594
  /**
@@ -544,7 +606,39 @@ function resolveBuildContext(configDir, buildContext) {
544
606
  }
545
607
 
546
608
  /**
547
- * Gets the builder folder path. Uses AIFABRIX_BUILDER_DIR when set, else project root.
609
+ * `cwd/builder/<appName>` when present as a directory and allowed (platform empty stubs skipped).
610
+ *
611
+ * @param {string} appName
612
+ * @returns {string|null}
613
+ */
614
+ function tryCwdBuilderPathOrNull(appName) {
615
+ const cwdApp = path.join(path.resolve(process.cwd()), 'builder', appName);
616
+ try {
617
+ if (!nodeFs().existsSync(cwdApp)) {
618
+ return null;
619
+ }
620
+ const st = nodeFs().statSync(cwdApp);
621
+ if (!st || typeof st.isDirectory !== 'function' || !st.isDirectory()) {
622
+ return null;
623
+ }
624
+ if (isSystemBuilderAppName(appName) && !isProjectBuilderAppDirectory(cwdApp)) {
625
+ return null;
626
+ }
627
+ return cwdApp;
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Gets the builder folder path:
635
+ * 1. Plan 141 Tier‑1: `cwd/builder/<app>` or `cwd/integration/<app>` when a resolvable application manifest exists.
636
+ * 2. Else **`cwd/builder/<app>`** when that directory exists. For **platform** apps (`keycloak`,
637
+ * `miso-controller`, `dataplane`), an **empty** cwd stub is ignored so materialization under
638
+ * `(work|home)/builder` still wins.
639
+ * 3. Else **`(aifabrix-work or aifabrix-home)/builder/<appName>`** — used by setup / `up-platform` /
640
+ * `up-miso` / `up-dataplane` for template materialization.
641
+ *
548
642
  * @param {string} appName - Application name
549
643
  * @returns {string} Absolute path to builder directory
550
644
  */
@@ -552,21 +646,20 @@ function getBuilderPath(appName) {
552
646
  if (!appName || typeof appName !== 'string') {
553
647
  throw new Error('App name is required and must be a string');
554
648
  }
555
- const envBuilderRoot = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
556
- ? process.env.AIFABRIX_BUILDER_DIR.trim()
557
- : null;
558
- if (envBuilderRoot) {
559
- return path.join(path.resolve(envBuilderRoot), appName);
649
+ const { resolveApplicationManifestPathSync } = require('./manifest-location');
650
+ const manifestHit = resolveApplicationManifestPathSync({
651
+ targetKey: appName,
652
+ mode: 'auto',
653
+ cwd: process.cwd()
654
+ });
655
+ if (manifestHit && (manifestHit.tier === 'cwd-builder' || manifestHit.tier === 'cwd-integration')) {
656
+ return manifestHit.absolutePath;
560
657
  }
561
- if (isSystemBuilderAppName(appName)) {
562
- const projectAppPath = getProjectBuilderAppPath(appName);
563
- if (isProjectBuilderAppDirectory(projectAppPath)) {
564
- return projectAppPath;
565
- }
566
- return path.join(getSystemBuilderRoot(), appName);
658
+ const cwdHit = tryCwdBuilderPathOrNull(appName);
659
+ if (cwdHit) {
660
+ return cwdHit;
567
661
  }
568
- const base = getIntegrationBuilderBaseDir();
569
- return path.join(base, 'builder', appName);
662
+ return path.join(getAppsMaterializationParent(), 'builder', appName);
570
663
  }
571
664
 
572
665
  /**
@@ -717,16 +810,66 @@ async function getResolveAppPath(appName) {
717
810
  return { appPath: integrationPath, envOnly: true };
718
811
  }
719
812
  }
813
+ const { resolveApplicationManifestPathSync } = require('./manifest-location');
814
+ const manifestHit = resolveApplicationManifestPathSync({
815
+ targetKey: appName,
816
+ mode: 'auto',
817
+ cwd: process.cwd()
818
+ });
819
+ if (manifestHit) {
820
+ return { appPath: manifestHit.absolutePath, envOnly: false };
821
+ }
720
822
  const result = await detectAppType(appName);
721
823
  return { appPath: result.appPath, envOnly: false };
722
824
  }
723
825
 
724
- /** Resolve app folder name when cwd is inside integration/<systemKey>/. */
826
+ /**
827
+ * @param {string} walkDir - Candidate workspace root (walk upward from cwd)
828
+ * @param {string} cwd - Resolved process.cwd()
829
+ * @param {ReturnType<typeof nodeFs>} disk
830
+ * @returns {string|null}
831
+ */
832
+ function tryIntegrationAppKeyAtWalkStep(walkDir, cwd, disk) {
833
+ const integrationDir = path.join(walkDir, 'integration');
834
+ try {
835
+ if (!disk.existsSync(integrationDir)) {
836
+ return null;
837
+ }
838
+ const intNorm = path.resolve(integrationDir);
839
+ if (cwd !== intNorm && !cwd.startsWith(intNorm + path.sep)) {
840
+ return null;
841
+ }
842
+ const rel = path.relative(intNorm, cwd);
843
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
844
+ return null;
845
+ }
846
+ return rel.split(path.sep)[0] || null;
847
+ } catch {
848
+ return null;
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Resolve app folder name when cwd is under some ancestor's `integration/<systemKey>/`.
854
+ * Walks up from cwd (does not use {@link getIntegrationBuilderBaseDir}) so detection still works
855
+ * when the canonical base is config/home but the shell is inside a checkout's integration tree.
856
+ */
725
857
  function resolveIntegrationAppKeyFromCwd() {
726
- const integrationNorm = path.resolve(path.join(getIntegrationBuilderBaseDir(), 'integration'));
727
858
  const cwd = path.resolve(process.cwd());
728
- if (cwd !== integrationNorm && !cwd.startsWith(integrationNorm + path.sep)) return null;
729
- return path.relative(integrationNorm, cwd).split(path.sep)[0] || null;
859
+ const disk = nodeFs();
860
+ let p = cwd;
861
+ for (let i = 0; i < 64; i += 1) {
862
+ const key = tryIntegrationAppKeyAtWalkStep(p, cwd, disk);
863
+ if (key) {
864
+ return key;
865
+ }
866
+ const parent = path.dirname(p);
867
+ if (parent === p) {
868
+ break;
869
+ }
870
+ p = parent;
871
+ }
872
+ return null;
730
873
  }
731
874
 
732
875
  module.exports = {
@@ -738,14 +881,20 @@ module.exports = {
738
881
  getApplicationsBaseDir,
739
882
  getDevDirectory,
740
883
  getAppPath,
884
+ getAppsMaterializationParent,
885
+ getCwdIntegrationRoot,
886
+ getCwdBuilderRoot,
741
887
  getProjectRoot,
888
+ findProjectRootFromCwd,
742
889
  getIntegrationPath,
743
890
  getBuilderPath,
744
891
  getIntegrationRoot,
745
892
  getBuilderRoot,
893
+ getIntegrationBuilderBaseDir,
746
894
  SYSTEM_BUILDER_APP_KEYS,
747
895
  isSystemBuilderAppName,
748
896
  getProjectBuilderAppPath,
897
+ getSystemPlatformMaterializationParent,
749
898
  getSystemBuilderRoot,
750
899
  resolveSystemBuilderParentDir,
751
900
  listIntegrationAppNames,
@@ -54,14 +54,20 @@ async function loadRemoteSharedSecrets() {
54
54
  * Merges remote shared secrets with user secrets. User wins on same key.
55
55
  * @param {Object} userSecrets - User secrets object
56
56
  * @param {Object} remoteSecrets - Remote API secrets (key-value)
57
+ * @param {Record<string, string>} [keySources] - Mutated: winning file/API label per key (decrypt hints)
58
+ * @param {string} [remoteSourceLabel] - Human-readable source for keys taken from remote
57
59
  * @returns {Object} Merged object
58
60
  */
59
- function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets) {
61
+ function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets, keySources, remoteSourceLabel) {
60
62
  const merged = { ...userSecrets };
61
63
  if (!remoteSecrets || typeof remoteSecrets !== 'object') return merged;
64
+ const label = remoteSourceLabel || 'shared secrets API';
62
65
  for (const key of Object.keys(remoteSecrets)) {
63
66
  if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
64
67
  merged[key] = remoteSecrets[key];
68
+ if (keySources) {
69
+ keySources[key] = label;
70
+ }
65
71
  }
66
72
  }
67
73
  return merged;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Normalize Commander flags for `aifabrix run`.
3
+ *
4
+ * @fileoverview Commander v11 pairs `--no-proxy` with a default-true `--proxy` flag as `options.proxy === false`.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * True when the user disabled proxy hints: explicit `--no-proxy` or `proxy === false` when supported.
13
+ *
14
+ * @param {Object} [options] - Commander action options
15
+ * @returns {boolean}
16
+ */
17
+ function isRunCliNoProxy(options) {
18
+ if (!options || typeof options !== 'object') {
19
+ return false;
20
+ }
21
+ if (options.proxy === false) {
22
+ return true;
23
+ }
24
+ return options.noProxy === true;
25
+ }
26
+
27
+ module.exports = {
28
+ isRunCliNoProxy
29
+ };
@@ -31,11 +31,14 @@ function readYamlAtPath(filePath) {
31
31
  * @param {string} key - Secret key
32
32
  * @param {*} canonicalValue - Value from canonical secrets
33
33
  */
34
- function mergeSecretValue(result, key, canonicalValue) {
34
+ function mergeSecretValue(result, key, canonicalValue, keySources, canonicalSourcePath) {
35
35
  const currentValue = result[key];
36
36
  // Fill missing, empty, or undefined values
37
37
  if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
38
38
  result[key] = canonicalValue;
39
+ if (keySources && canonicalSourcePath) {
40
+ keySources[key] = canonicalSourcePath;
41
+ }
39
42
  return;
40
43
  }
41
44
  // Only replace values that are encrypted (have secure:// prefix)
@@ -43,6 +46,9 @@ function mergeSecretValue(result, key, canonicalValue) {
43
46
  if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
44
47
  if (currentValue.startsWith('secure://')) {
45
48
  result[key] = canonicalValue;
49
+ if (keySources && canonicalSourcePath) {
50
+ keySources[key] = canonicalSourcePath;
51
+ }
46
52
  }
47
53
  }
48
54
  }
@@ -52,9 +58,10 @@ function mergeSecretValue(result, key, canonicalValue) {
52
58
  * @async
53
59
  * @function applyCanonicalSecretsOverride
54
60
  * @param {Object} currentSecrets - Current secrets map
61
+ * @param {Record<string, string>} [keySources] - Mutated: per-key source path when a value is taken from canonical YAML
55
62
  * @returns {Promise<Object>} Possibly overridden secrets
56
63
  */
57
- async function applyCanonicalSecretsOverride(currentSecrets) {
64
+ async function applyCanonicalSecretsOverride(currentSecrets, keySources) {
58
65
  let mergedSecrets = currentSecrets || {};
59
66
  try {
60
67
  const canonicalPath = await config.getSecretsPath();
@@ -78,7 +85,7 @@ async function applyCanonicalSecretsOverride(currentSecrets) {
78
85
  // - Replace encrypted values (secure://) with canonical plaintext
79
86
  const result = { ...mergedSecrets };
80
87
  for (const [key, canonicalValue] of Object.entries(configSecrets)) {
81
- mergeSecretValue(result, key, canonicalValue);
88
+ mergeSecretValue(result, key, canonicalValue, keySources, resolvedCanonical);
82
89
  }
83
90
  mergedSecrets = result;
84
91
  } catch {
@@ -41,9 +41,8 @@ function resolveSecretsPath(secretsPath) {
41
41
  }
42
42
 
43
43
  /**
44
- * Determines the actual secrets file paths that loadSecrets would use
45
- * Mirrors the cascading lookup logic from loadSecrets
46
- * Uses config.yaml for default secrets path as fallback
44
+ * Determines paths used for default `loadSecrets()` (no explicit path): primary user secrets file
45
+ * and configured `aifabrix-secrets` (shared YAML file path or remote API URL).
47
46
  *
48
47
  * @async
49
48
  * @function getActualSecretsPath
@@ -65,7 +64,7 @@ async function getActualSecretsPath(secretsPath, _appName) {
65
64
  };
66
65
  }
67
66
 
68
- // Cascading lookup: user's file first (same path as `secret set`: getPrimaryUserSecretsLocalPath)
67
+ // Default lookup: primary user file plus aifabrix-secrets (file or https URL)
69
68
  const userSecretsPath = paths.getPrimaryUserSecretsLocalPath();
70
69
 
71
70
  let buildSecretsPath = null;
@@ -58,18 +58,14 @@ async function loadSecretsFromFile(filePath) {
58
58
  }
59
59
 
60
60
  /**
61
- * Loads user secrets from getPrimaryUserSecretsLocalPath() (under {@link module:lib/utils/paths.getAifabrixHome}).
62
- * Used as the master source when merging with project/public secrets: user values win,
63
- * missing keys are filled from the public (aifabrix-secrets) file.
61
+ * Loads user secrets from {@link pathsUtil.getPrimaryUserSecretsLocalPath} (beside `config.yaml`).
62
+ * If that file is missing, reads a legacy file at `getAifabrixHome()/secrets.local.yaml` when paths
63
+ * differ (older CLI when `aifabrix-home` was POSIX home).
64
64
  *
65
65
  * @function loadPrimaryUserSecrets
66
66
  * @returns {Object} Loaded secrets object or empty object
67
67
  */
68
- function loadPrimaryUserSecrets() {
69
- const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
70
- if (!fs.existsSync(userSecretsPath)) {
71
- return {};
72
- }
68
+ function readPrimaryUserSecretsAtPath(userSecretsPath) {
73
69
  ensureSecureFilePermissions(userSecretsPath);
74
70
 
75
71
  try {
@@ -84,8 +80,22 @@ function loadPrimaryUserSecrets() {
84
80
  throw error;
85
81
  }
86
82
  logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
87
- return {};
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function loadPrimaryUserSecrets() {
88
+ const primaryPath = pathsUtil.getPrimaryUserSecretsLocalPath();
89
+ if (fs.existsSync(primaryPath)) {
90
+ const fromPrimary = readPrimaryUserSecretsAtPath(primaryPath);
91
+ return fromPrimary !== null ? fromPrimary : {};
92
+ }
93
+ const legacyPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
94
+ if (path.resolve(legacyPath) !== path.resolve(primaryPath) && fs.existsSync(legacyPath)) {
95
+ const fromLegacy = readPrimaryUserSecretsAtPath(legacyPath);
96
+ return fromLegacy !== null ? fromLegacy : {};
88
97
  }
98
+ return {};
89
99
  }
90
100
 
91
101
  /**
@@ -128,7 +138,7 @@ function loadDefaultSecrets() {
128
138
 
129
139
  /**
130
140
  * Creates the primary user secrets file if missing (empty map) for first-run installs.
131
- * Uses the same directory as {@link loadPrimaryUserSecrets} (resolved AI Fabrix home).
141
+ * Uses the same directory as {@link loadPrimaryUserSecrets} (beside `config.yaml`).
132
142
  *
133
143
  * @function ensurePrimaryUserSecretsFileExists
134
144
  */