@aifabrix/builder 2.44.5 → 2.44.6
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/.cursor/rules/cli-layout.mdc +1 -1
- package/.cursor/rules/project-rules.mdc +1 -1
- package/.npmrc.token +1 -1
- package/README.md +15 -23
- package/integration/hubspot-test/README.md +2 -0
- package/integration/hubspot-test/test.js +5 -3
- package/jest.projects.js +48 -2
- package/lib/api/controller-health.api.js +49 -0
- package/lib/api/dimension-values.api.js +82 -0
- package/lib/api/dimensions.api.js +114 -0
- package/lib/api/external-systems.api.js +1 -0
- package/lib/api/integration-clients.api.js +168 -0
- package/lib/api/types/dimension-values.types.js +28 -0
- package/lib/api/types/dimensions.types.js +31 -0
- package/lib/api/types/integration-clients.types.js +45 -0
- package/lib/api/validation-runner.js +46 -25
- package/lib/app/deploy-config.js +11 -1
- package/lib/app/deploy-status-display.js +3 -3
- package/lib/app/deploy.js +36 -14
- package/lib/app/display.js +15 -11
- package/lib/app/push.js +46 -23
- package/lib/app/register.js +1 -1
- package/lib/app/restart-display.js +95 -0
- package/lib/app/rotate-secret.js +1 -1
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +44 -12
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +99 -73
- package/lib/build/index.js +75 -45
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +445 -0
- package/lib/cli/setup-app.js +20 -2
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +134 -61
- package/lib/cli/setup-integration-client.js +182 -0
- package/lib/cli/setup-parameters.js +21 -2
- package/lib/cli/setup-platform.js +102 -0
- package/lib/cli/setup-secrets.js +18 -6
- package/lib/cli/setup-utility.js +78 -33
- package/lib/commands/datasource-capability-dimension-cli.js +128 -0
- package/lib/commands/datasource-capability-output.js +29 -0
- package/lib/commands/datasource-capability-relate-cli.js +140 -0
- package/lib/commands/datasource-capability.js +411 -0
- package/lib/commands/datasource-unified-test-cli.options.js +1 -1
- package/lib/commands/datasource.js +53 -13
- package/lib/commands/dev-down.js +3 -3
- package/lib/commands/dev-infra-gate.js +32 -0
- package/lib/commands/dev-init.js +13 -7
- package/lib/commands/dimension-value.js +179 -0
- package/lib/commands/dimension.js +330 -0
- package/lib/commands/integration-client.js +430 -0
- package/lib/commands/login-device.js +65 -30
- package/lib/commands/login.js +21 -10
- package/lib/commands/parameters-validate.js +78 -13
- package/lib/commands/repair-datasource-auto-rbac.js +166 -0
- package/lib/commands/repair-datasource-keys.js +10 -5
- package/lib/commands/repair-datasource.js +19 -7
- package/lib/commands/repair-env-template.js +4 -1
- package/lib/commands/repair-openapi-sync.js +172 -0
- package/lib/commands/repair-persist.js +102 -0
- package/lib/commands/repair-rbac-extract.js +27 -0
- package/lib/commands/repair-rbac-migrate.js +186 -0
- package/lib/commands/repair-rbac.js +214 -31
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -338
- package/lib/commands/secure.js +1 -1
- package/lib/commands/setup-modes.js +455 -0
- package/lib/commands/setup-prompts.js +388 -0
- package/lib/commands/setup.js +149 -0
- package/lib/commands/teardown.js +228 -0
- package/lib/commands/up-common.js +79 -19
- package/lib/commands/up-dataplane.js +33 -11
- package/lib/commands/up-miso.js +7 -11
- package/lib/commands/upload.js +109 -23
- package/lib/commands/wizard-core-helpers.js +14 -11
- package/lib/commands/wizard-core.js +6 -5
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +4 -3
- package/lib/commands/wizard-headless.js +2 -1
- package/lib/commands/wizard.js +2 -1
- package/lib/constants/infra-compose-service-names.js +40 -0
- package/lib/core/env-reader.js +16 -3
- package/lib/core/secrets-admin-env.js +101 -0
- package/lib/core/secrets-ensure-infra.js +34 -1
- package/lib/core/secrets-ensure.js +88 -66
- package/lib/core/secrets-env-content.js +432 -0
- package/lib/core/secrets-env-write.js +27 -1
- package/lib/core/secrets-load.js +248 -0
- package/lib/core/secrets-names.js +32 -0
- package/lib/core/secrets.js +17 -757
- package/lib/datasource/capability/basic-exposure.js +76 -0
- package/lib/datasource/capability/capability-diff-slice.js +41 -0
- package/lib/datasource/capability/capability-key.js +34 -0
- package/lib/datasource/capability/capability-resolve.js +172 -0
- package/lib/datasource/capability/capability-storage-keys.js +22 -0
- package/lib/datasource/capability/copy-operations.js +348 -0
- package/lib/datasource/capability/copy-test-payload.js +139 -0
- package/lib/datasource/capability/create-operations.js +235 -0
- package/lib/datasource/capability/dimension-operations.js +151 -0
- package/lib/datasource/capability/dimension-validate.js +219 -0
- package/lib/datasource/capability/json-pointer.js +31 -0
- package/lib/datasource/capability/reference-rewrite.js +51 -0
- package/lib/datasource/capability/relate-operations.js +325 -0
- package/lib/datasource/capability/relate-validate.js +219 -0
- package/lib/datasource/capability/remove-operations.js +275 -0
- package/lib/datasource/capability/run-capability-copy.js +152 -0
- package/lib/datasource/capability/run-capability-diff.js +135 -0
- package/lib/datasource/capability/run-capability-dimension.js +291 -0
- package/lib/datasource/capability/run-capability-edit.js +377 -0
- package/lib/datasource/capability/run-capability-relate.js +193 -0
- package/lib/datasource/capability/run-capability-remove.js +105 -0
- package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
- package/lib/datasource/capability/validate-capability-slice.js +35 -0
- package/lib/datasource/list.js +136 -23
- package/lib/datasource/log-viewer.js +2 -4
- package/lib/datasource/unified-validation-run.js +51 -16
- package/lib/datasource/validate.js +53 -1
- package/lib/deployment/deploy-poll-ui.js +60 -0
- package/lib/deployment/deployer-status.js +29 -3
- package/lib/deployment/deployer.js +48 -30
- package/lib/deployment/environment.js +7 -2
- package/lib/deployment/poll-interval.js +72 -0
- package/lib/deployment/push.js +11 -9
- package/lib/external-system/deploy.js +4 -1
- package/lib/external-system/download.js +61 -32
- package/lib/external-system/sync-deploy-manifest.js +33 -0
- package/lib/infrastructure/index.js +49 -19
- package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
- package/lib/parameters/infra-kv-discovery.js +29 -4
- package/lib/parameters/infra-parameter-catalog.js +6 -3
- package/lib/parameters/infra-parameter-validate.js +67 -19
- package/lib/resolvers/datasource-resolver.js +53 -0
- package/lib/resolvers/dimension-file.js +52 -0
- package/lib/resolvers/manifest-resolver.js +133 -0
- package/lib/schema/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -11
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/aifabrix-config-dir-walk.js +40 -0
- package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
- package/lib/utils/app-run-containers.js +2 -2
- package/lib/utils/bash-secret-env.js +59 -0
- package/lib/utils/cli-secrets-error-format.js +78 -0
- package/lib/utils/cli-test-layout-chalk.js +31 -9
- package/lib/utils/cli-utils.js +4 -36
- package/lib/utils/datasource-test-run-display.js +8 -0
- package/lib/utils/dev-hosts-helper.js +3 -2
- package/lib/utils/dev-init-ssh-merge.js +2 -1
- package/lib/utils/docker-build.js +17 -9
- package/lib/utils/docker-reload-mount.js +127 -0
- package/lib/utils/external-readme.js +71 -2
- package/lib/utils/external-system-local-test-tty.js +3 -2
- package/lib/utils/external-system-readiness-core.js +45 -12
- package/lib/utils/external-system-readiness-deploy-display.js +3 -3
- package/lib/utils/external-system-readiness-display-internals.js +33 -3
- package/lib/utils/external-system-readiness-display.js +10 -1
- package/lib/utils/file-upload.js +40 -3
- package/lib/utils/health-check-db-init.js +107 -0
- package/lib/utils/health-check-public-warn.js +69 -0
- package/lib/utils/health-check-url.js +19 -4
- package/lib/utils/health-check.js +135 -105
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +108 -25
- package/lib/utils/postgres-wipe.js +212 -0
- package/lib/utils/register-aifabrix-shell-env.js +15 -0
- package/lib/utils/remote-dev-auth.js +21 -5
- package/lib/utils/remote-docker-env.js +9 -1
- package/lib/utils/remote-secrets-loader.js +42 -3
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- package/lib/utils/secrets-helpers.js +17 -10
- package/lib/utils/secrets-kv-refs.js +42 -0
- package/lib/utils/secrets-kv-scope.js +19 -2
- package/lib/utils/secrets-materialize-local.js +134 -0
- package/lib/utils/secrets-path.js +24 -10
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/system-builder-root.js +34 -0
- package/lib/utils/url-declarative-resolve-build.js +6 -1
- package/lib/utils/url-declarative-runtime-base-path.js +32 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry.js +23 -12
- package/lib/utils/validation-poll-ui.js +81 -0
- package/lib/utils/validation-run-poll.js +29 -5
- package/lib/utils/with-muted-logger.js +53 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +7 -0
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +83 -123
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/lib/api/service-users.api.js +0 -150
- package/lib/api/types/service-users.types.js +0 -65
- package/lib/cli/setup-service-user.js +0 -187
- package/lib/commands/service-user.js +0 -429
package/lib/utils/paths.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
getAifabrixRuntimeConfigDir,
|
|
18
18
|
resolveAifabrixHomeLikePath
|
|
19
19
|
} = require('./aifabrix-runtime-config-dir');
|
|
20
|
+
const { resolveSystemBuilderParentDir } = require('./system-builder-root');
|
|
20
21
|
|
|
21
22
|
function safeHomedir() {
|
|
22
23
|
try {
|
|
@@ -43,14 +44,13 @@ function getConfigDirForPaths() {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* User-owned `secrets.local.yaml`
|
|
47
|
-
*
|
|
48
|
-
* Keeps `secret list` / `secret set` aligned with resolve merge (`loadPrimaryUserSecrets`).
|
|
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.
|
|
49
49
|
*
|
|
50
50
|
* @returns {string} Absolute path to secrets.local.yaml
|
|
51
51
|
*/
|
|
52
52
|
function getPrimaryUserSecretsLocalPath() {
|
|
53
|
-
return path.join(
|
|
53
|
+
return path.join(getAifabrixHome(), 'secrets.local.yaml');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -327,7 +327,7 @@ function getAppPath(appName, appType) {
|
|
|
327
327
|
if (appType === 'external') {
|
|
328
328
|
return getIntegrationPath(appName);
|
|
329
329
|
}
|
|
330
|
-
return
|
|
330
|
+
return getBuilderPath(appName);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
/**
|
|
@@ -342,7 +342,7 @@ function getIntegrationBuilderBaseDir() {
|
|
|
342
342
|
const workNorm = path.resolve(work);
|
|
343
343
|
const integrationUnderWork = path.join(workNorm, 'integration');
|
|
344
344
|
try {
|
|
345
|
-
if (
|
|
345
|
+
if (nodeFs().existsSync(integrationUnderWork)) {
|
|
346
346
|
return workNorm;
|
|
347
347
|
}
|
|
348
348
|
} catch {
|
|
@@ -380,6 +380,60 @@ function getBuilderRoot() {
|
|
|
380
380
|
return path.join(getIntegrationBuilderBaseDir(), 'builder');
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/**
|
|
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}).
|
|
387
|
+
* @readonly
|
|
388
|
+
*/
|
|
389
|
+
const SYSTEM_BUILDER_APP_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane']);
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @param {string} appName
|
|
393
|
+
* @returns {boolean}
|
|
394
|
+
*/
|
|
395
|
+
function isSystemBuilderAppName(appName) {
|
|
396
|
+
if (!appName || typeof appName !== 'string') return false;
|
|
397
|
+
return SYSTEM_BUILDER_APP_KEYS.includes(appName);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Project/cwd `builder/<appName>` (no existence check).
|
|
402
|
+
* @param {string} appName
|
|
403
|
+
* @returns {string}
|
|
404
|
+
*/
|
|
405
|
+
function getProjectBuilderAppPath(appName) {
|
|
406
|
+
return path.join(getIntegrationBuilderBaseDir(), 'builder', appName);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
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).
|
|
413
|
+
*
|
|
414
|
+
* @returns {string}
|
|
415
|
+
*/
|
|
416
|
+
function getSystemBuilderRoot() {
|
|
417
|
+
return path.join(
|
|
418
|
+
resolveSystemBuilderParentDir(getAifabrixSystemDir(), getAifabrixHome()),
|
|
419
|
+
'builder'
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* @param {string} projectAppPath - Absolute `.../builder/<app>`
|
|
425
|
+
* @returns {boolean}
|
|
426
|
+
*/
|
|
427
|
+
function isProjectBuilderAppDirectory(projectAppPath) {
|
|
428
|
+
try {
|
|
429
|
+
if (!projectAppPath || !nodeFs().existsSync(projectAppPath)) return false;
|
|
430
|
+
const st = nodeFs().statSync(projectAppPath);
|
|
431
|
+
return Boolean(st && typeof st.isDirectory === 'function' && st.isDirectory());
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
383
437
|
/**
|
|
384
438
|
* True when name under root is a real directory (follows symlinks). False for broken symlinks, files, ENOENT.
|
|
385
439
|
* @param {string} root - Parent directory (must exist)
|
|
@@ -422,27 +476,44 @@ function listIntegrationAppNames() {
|
|
|
422
476
|
}
|
|
423
477
|
|
|
424
478
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* @
|
|
479
|
+
* Adds immediate subdirectory names under a builder root into `names` (real disk via {@link nodeFs}).
|
|
480
|
+
* @param {ReturnType<typeof nodeFs>} disk
|
|
481
|
+
* @param {string} builderRootDir
|
|
482
|
+
* @param {Set<string>} names
|
|
483
|
+
* @param {(name: string) => boolean} [nameFilter] - When set, only names passing this filter (after dir check)
|
|
428
484
|
*/
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (!disk.existsSync(root)) {
|
|
433
|
-
return [];
|
|
434
|
-
}
|
|
435
|
-
let rootStat;
|
|
485
|
+
function addBuilderSubdirNamesToSet(disk, builderRootDir, names, nameFilter) {
|
|
486
|
+
if (!builderRootDir || !disk.existsSync(builderRootDir)) return;
|
|
487
|
+
let st;
|
|
436
488
|
try {
|
|
437
|
-
|
|
489
|
+
st = disk.statSync(builderRootDir);
|
|
438
490
|
} catch {
|
|
439
|
-
return
|
|
491
|
+
return;
|
|
440
492
|
}
|
|
441
|
-
if (!
|
|
442
|
-
|
|
493
|
+
if (!st || typeof st.isDirectory !== 'function' || !st.isDirectory()) return;
|
|
494
|
+
try {
|
|
495
|
+
for (const name of disk.readdirSync(builderRootDir)) {
|
|
496
|
+
if (!isAppSubdirSync(builderRootDir, name)) continue;
|
|
497
|
+
if (nameFilter && !nameFilter(name)) continue;
|
|
498
|
+
names.add(name);
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
// ignore
|
|
443
502
|
}
|
|
444
|
-
|
|
445
|
-
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
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).
|
|
508
|
+
* @returns {string[]} Sorted list of app directory names
|
|
509
|
+
*/
|
|
510
|
+
function listBuilderAppNames() {
|
|
511
|
+
const disk = nodeFs();
|
|
512
|
+
const names = new Set();
|
|
513
|
+
const projectBuilder = path.join(getIntegrationBuilderBaseDir(), 'builder');
|
|
514
|
+
addBuilderSubdirNamesToSet(disk, projectBuilder, names, null);
|
|
515
|
+
addBuilderSubdirNamesToSet(disk, getSystemBuilderRoot(), names, isSystemBuilderAppName);
|
|
516
|
+
return [...names].sort();
|
|
446
517
|
}
|
|
447
518
|
|
|
448
519
|
/**
|
|
@@ -481,11 +552,18 @@ function getBuilderPath(appName) {
|
|
|
481
552
|
if (!appName || typeof appName !== 'string') {
|
|
482
553
|
throw new Error('App name is required and must be a string');
|
|
483
554
|
}
|
|
484
|
-
const
|
|
555
|
+
const envBuilderRoot = process.env.AIFABRIX_BUILDER_DIR && typeof process.env.AIFABRIX_BUILDER_DIR === 'string'
|
|
485
556
|
? process.env.AIFABRIX_BUILDER_DIR.trim()
|
|
486
557
|
: null;
|
|
487
|
-
if (
|
|
488
|
-
return path.join(path.resolve(
|
|
558
|
+
if (envBuilderRoot) {
|
|
559
|
+
return path.join(path.resolve(envBuilderRoot), appName);
|
|
560
|
+
}
|
|
561
|
+
if (isSystemBuilderAppName(appName)) {
|
|
562
|
+
const projectAppPath = getProjectBuilderAppPath(appName);
|
|
563
|
+
if (isProjectBuilderAppDirectory(projectAppPath)) {
|
|
564
|
+
return projectAppPath;
|
|
565
|
+
}
|
|
566
|
+
return path.join(getSystemBuilderRoot(), appName);
|
|
489
567
|
}
|
|
490
568
|
const base = getIntegrationBuilderBaseDir();
|
|
491
569
|
return path.join(base, 'builder', appName);
|
|
@@ -665,6 +743,11 @@ module.exports = {
|
|
|
665
743
|
getBuilderPath,
|
|
666
744
|
getIntegrationRoot,
|
|
667
745
|
getBuilderRoot,
|
|
746
|
+
SYSTEM_BUILDER_APP_KEYS,
|
|
747
|
+
isSystemBuilderAppName,
|
|
748
|
+
getProjectBuilderAppPath,
|
|
749
|
+
getSystemBuilderRoot,
|
|
750
|
+
resolveSystemBuilderParentDir,
|
|
668
751
|
listIntegrationAppNames,
|
|
669
752
|
listBuilderAppNames,
|
|
670
753
|
resolveBuildContext,
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres data wipe helper for `aifabrix setup` Mode 2 (Wipe data).
|
|
3
|
+
*
|
|
4
|
+
* Drops every non-template database and every non-superuser role in the
|
|
5
|
+
* developer's running Postgres container, while preserving the volume,
|
|
6
|
+
* the `postgres` superuser, and the admin password (so the post-wipe
|
|
7
|
+
* `up-infra` and platform bootstrap recreate schemas + service users).
|
|
8
|
+
*
|
|
9
|
+
* Uses `docker exec` with the admin password passed via the container's
|
|
10
|
+
* environment (PGPASSWORD) so it is not visible in `ps`.
|
|
11
|
+
*
|
|
12
|
+
* @fileoverview Drop all DBs and roles in dev Postgres for setup Mode 2
|
|
13
|
+
* @author AI Fabrix Team
|
|
14
|
+
* @version 2.0.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const config = require('../core/config');
|
|
20
|
+
const adminSecrets = require('../core/admin-secrets');
|
|
21
|
+
const dockerExec = require('./docker-exec');
|
|
22
|
+
const logger = require('./logger');
|
|
23
|
+
const chalk = require('chalk');
|
|
24
|
+
const { successGlyph } = require('./cli-test-layout-chalk');
|
|
25
|
+
|
|
26
|
+
/** Roles that must never be dropped (Postgres-managed predefined roles). */
|
|
27
|
+
const PROTECTED_ROLES = new Set([
|
|
28
|
+
// In this project, infra starts Postgres with POSTGRES_USER=pgadmin (see templates/infra/compose.yaml.hbs),
|
|
29
|
+
// so that role is the superuser we must preserve.
|
|
30
|
+
'pgadmin',
|
|
31
|
+
// Legacy / default superuser name for official Postgres images.
|
|
32
|
+
'postgres',
|
|
33
|
+
'pg_signal_backend',
|
|
34
|
+
'pg_read_server_files',
|
|
35
|
+
'pg_write_server_files',
|
|
36
|
+
'pg_execute_server_program',
|
|
37
|
+
'pg_monitor',
|
|
38
|
+
'pg_read_all_settings',
|
|
39
|
+
'pg_read_all_stats',
|
|
40
|
+
'pg_stat_scan_tables',
|
|
41
|
+
'pg_database_owner',
|
|
42
|
+
'pg_read_all_data',
|
|
43
|
+
'pg_write_all_data',
|
|
44
|
+
'pg_checkpoint',
|
|
45
|
+
'pg_use_reserved_connections',
|
|
46
|
+
'pg_create_subscription'
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/** Databases that must never be dropped. */
|
|
50
|
+
const PROTECTED_DATABASES = new Set(['postgres', 'template0', 'template1']);
|
|
51
|
+
|
|
52
|
+
/** Superuser role used inside the infra Postgres container. */
|
|
53
|
+
const SUPERUSER_ROLE = 'pgadmin';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compute the developer-scoped Postgres container name.
|
|
57
|
+
* Mirrors `getInfraContainerNames` in `lib/utils/infra-status.js`.
|
|
58
|
+
* @param {number|string} devId - Developer ID
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function getPostgresContainerName(devId) {
|
|
62
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
63
|
+
if (idNum === 0) return 'aifabrix-postgres';
|
|
64
|
+
return `aifabrix-dev${devId}-postgres`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run `psql -t -A -c <sql>` inside the Postgres container with PGPASSWORD set
|
|
69
|
+
* via `-e PGPASSWORD=...` so the secret never appears on the command line.
|
|
70
|
+
*
|
|
71
|
+
* @async
|
|
72
|
+
* @param {string} container - Container name
|
|
73
|
+
* @param {string} sql - Single SQL statement (no shell metacharacters)
|
|
74
|
+
* @param {string} adminPassword - Postgres superuser password
|
|
75
|
+
* @returns {Promise<string>} stdout (trimmed)
|
|
76
|
+
* @throws {Error} If the docker exec invocation fails
|
|
77
|
+
*/
|
|
78
|
+
async function runPsql(container, sql, adminPassword) {
|
|
79
|
+
if (!container || typeof container !== 'string') {
|
|
80
|
+
throw new Error('Postgres container name is required');
|
|
81
|
+
}
|
|
82
|
+
if (!sql || typeof sql !== 'string') {
|
|
83
|
+
throw new Error('SQL statement is required');
|
|
84
|
+
}
|
|
85
|
+
if (!adminPassword || typeof adminPassword !== 'string') {
|
|
86
|
+
throw new Error('Admin password is required');
|
|
87
|
+
}
|
|
88
|
+
const escapedSql = sql.replace(/"/g, '\\"');
|
|
89
|
+
const cmd = `docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "${escapedSql}"`;
|
|
90
|
+
const env = { PGPASSWORD: adminPassword };
|
|
91
|
+
const result = (await dockerExec.execWithDockerEnv(cmd, { env })) || {};
|
|
92
|
+
return String(result.stdout || '').trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* List user databases (non-template, not in PROTECTED_DATABASES).
|
|
97
|
+
* @async
|
|
98
|
+
* @param {string} container - Container name
|
|
99
|
+
* @param {string} adminPassword - Postgres superuser password
|
|
100
|
+
* @returns {Promise<string[]>} database names
|
|
101
|
+
*/
|
|
102
|
+
async function listUserDatabases(container, adminPassword) {
|
|
103
|
+
const out = await runPsql(
|
|
104
|
+
container,
|
|
105
|
+
'SELECT datname FROM pg_database WHERE datistemplate = false;',
|
|
106
|
+
adminPassword
|
|
107
|
+
);
|
|
108
|
+
return out
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map(s => s.trim())
|
|
111
|
+
.filter(name => name && !PROTECTED_DATABASES.has(name));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* List dropable roles (non-superuser, not in PROTECTED_ROLES).
|
|
116
|
+
* @async
|
|
117
|
+
* @param {string} container - Container name
|
|
118
|
+
* @param {string} adminPassword - Postgres superuser password
|
|
119
|
+
* @returns {Promise<string[]>} role names
|
|
120
|
+
*/
|
|
121
|
+
async function listDropableRoles(container, adminPassword) {
|
|
122
|
+
const out = await runPsql(
|
|
123
|
+
container,
|
|
124
|
+
'SELECT rolname FROM pg_roles WHERE rolsuper = false;',
|
|
125
|
+
adminPassword
|
|
126
|
+
);
|
|
127
|
+
return out
|
|
128
|
+
.split('\n')
|
|
129
|
+
.map(s => s.trim())
|
|
130
|
+
.filter(name => name && !PROTECTED_ROLES.has(name) && !name.startsWith('pg_'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Drop a single database (forces disconnection of active sessions).
|
|
135
|
+
* @async
|
|
136
|
+
* @param {string} container
|
|
137
|
+
* @param {string} dbName
|
|
138
|
+
* @param {string} adminPassword
|
|
139
|
+
* @returns {Promise<void>}
|
|
140
|
+
*/
|
|
141
|
+
async function dropDatabase(container, dbName, adminPassword) {
|
|
142
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(dbName)) {
|
|
143
|
+
throw new Error(`Refusing to drop database with unsafe name: ${dbName}`);
|
|
144
|
+
}
|
|
145
|
+
await runPsql(
|
|
146
|
+
container,
|
|
147
|
+
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName}' AND pid <> pg_backend_pid();`,
|
|
148
|
+
adminPassword
|
|
149
|
+
);
|
|
150
|
+
await runPsql(container, `DROP DATABASE IF EXISTS "${dbName}";`, adminPassword);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Drop a single role (REASSIGN OWNED + DROP OWNED first to clear dependencies).
|
|
155
|
+
* @async
|
|
156
|
+
* @param {string} container
|
|
157
|
+
* @param {string} role
|
|
158
|
+
* @param {string} adminPassword
|
|
159
|
+
* @returns {Promise<void>}
|
|
160
|
+
*/
|
|
161
|
+
async function dropRole(container, role, adminPassword) {
|
|
162
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(role)) {
|
|
163
|
+
throw new Error(`Refusing to drop role with unsafe name: ${role}`);
|
|
164
|
+
}
|
|
165
|
+
await runPsql(
|
|
166
|
+
container,
|
|
167
|
+
`REASSIGN OWNED BY "${role}" TO ${SUPERUSER_ROLE};`,
|
|
168
|
+
adminPassword
|
|
169
|
+
).catch(() => undefined);
|
|
170
|
+
await runPsql(container, `DROP OWNED BY "${role}" CASCADE;`, adminPassword).catch(() => undefined);
|
|
171
|
+
await runPsql(container, `DROP ROLE IF EXISTS "${role}";`, adminPassword);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Drop every non-template database and every non-superuser role in the
|
|
176
|
+
* developer's running Postgres container. Caller must ensure infra is up.
|
|
177
|
+
*
|
|
178
|
+
* @async
|
|
179
|
+
* @function wipePostgresData
|
|
180
|
+
* @returns {Promise<{ databases: string[], roles: string[] }>} dropped names
|
|
181
|
+
* @throws {Error} If admin secrets are missing or psql calls fail
|
|
182
|
+
*/
|
|
183
|
+
async function wipePostgresData() {
|
|
184
|
+
const devId = await config.getDeveloperId();
|
|
185
|
+
const container = getPostgresContainerName(devId);
|
|
186
|
+
const admin = await adminSecrets.readAndDecryptAdminSecrets();
|
|
187
|
+
const password = admin && admin.POSTGRES_PASSWORD;
|
|
188
|
+
if (!password) {
|
|
189
|
+
throw new Error('POSTGRES_PASSWORD not found in admin-secrets.env. Run "aifabrix up-infra" first.');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const databases = await listUserDatabases(container, password);
|
|
193
|
+
for (const dbName of databases) {
|
|
194
|
+
await dropDatabase(container, dbName, password);
|
|
195
|
+
logger.log(chalk.gray(` ${successGlyph()} Dropped database ${dbName}`));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const roles = await listDropableRoles(container, password);
|
|
199
|
+
for (const role of roles) {
|
|
200
|
+
await dropRole(container, role, password);
|
|
201
|
+
logger.log(chalk.gray(` ${successGlyph()} Dropped role ${role}`));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { databases, roles };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
wipePostgresData,
|
|
209
|
+
getPostgresContainerName,
|
|
210
|
+
PROTECTED_DATABASES,
|
|
211
|
+
PROTECTED_ROLES
|
|
212
|
+
};
|
|
@@ -191,8 +191,23 @@ async function registerAifabrixShellEnvFromConfig(getConfigFn, overrides = {}) {
|
|
|
191
191
|
await applyPosixShellEnv(homeAbs, workAbs, overrides);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Build sh export lines for AIFABRIX_HOME / AIFABRIX_WORK from current config (stdout for eval).
|
|
196
|
+
*
|
|
197
|
+
* @async
|
|
198
|
+
* @param {function(): Promise<object>} getConfigFn - Same as config.getConfig
|
|
199
|
+
* @returns {Promise<string>} Lines suitable for `eval "$(aifabrix dev shell-env)"` on bash/zsh
|
|
200
|
+
*/
|
|
201
|
+
async function buildShellEnvExportsFromConfig(getConfigFn) {
|
|
202
|
+
const config = await getConfigFn();
|
|
203
|
+
const homeAbs = absFromConfigRaw(config['aifabrix-home']);
|
|
204
|
+
const workAbs = absFromConfigRaw(config['aifabrix-work']);
|
|
205
|
+
return buildPosixShellEnvBody(homeAbs, workAbs);
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
module.exports = {
|
|
195
209
|
registerAifabrixShellEnvFromConfig,
|
|
210
|
+
buildShellEnvExportsFromConfig,
|
|
196
211
|
buildPosixShellEnvBody,
|
|
197
212
|
buildProfileBlock,
|
|
198
213
|
shSingleQuoted,
|
|
@@ -5,10 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const config = require('../core/config');
|
|
9
10
|
const { getCertDir, readClientCertPem, readServerCaPem } = require('./dev-cert-helper');
|
|
10
11
|
const { getConfigDirForPaths, getAifabrixHome, getAifabrixWork } = require('./paths');
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Same rules as {@link module:lib/core/config.expandTilde} / {@link module:lib/core/config.getSecretsPath}.
|
|
15
|
+
* Inline here so `resolveSharedSecretsEndpoint` stays aligned with `getSecretsPath()` even when `getAifabrixSecretsPath()`
|
|
16
|
+
* returns the raw config string (tilde not expanded).
|
|
17
|
+
*/
|
|
18
|
+
function expandConfiguredSecretsTilde(filePath) {
|
|
19
|
+
if (!filePath || typeof filePath !== 'string') return filePath;
|
|
20
|
+
if (filePath === '~') return os.homedir();
|
|
21
|
+
if (filePath.startsWith('~/') || filePath.startsWith('~' + path.sep)) {
|
|
22
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
23
|
+
}
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
/**
|
|
13
28
|
* Single API object so resolveSharedSecretsEndpoint and callers share one getRemoteDevAuth
|
|
14
29
|
* (Jest spies and partial mocks work reliably).
|
|
@@ -54,16 +69,17 @@ const remoteDevAuth = {
|
|
|
54
69
|
const trimmed = configuredPath.trim();
|
|
55
70
|
if (!trimmed) return configuredPath;
|
|
56
71
|
if (remoteDevAuth.isRemoteSecretsUrl(trimmed)) return trimmed.replace(/\/+$/, '');
|
|
72
|
+
const expanded = expandConfiguredSecretsTilde(trimmed);
|
|
57
73
|
const auth = await remoteDevAuth.getRemoteDevAuth();
|
|
58
|
-
if (!auth) return
|
|
59
|
-
const abs = normalizeSharedSecretsFilePath(
|
|
60
|
-
if (!abs) return
|
|
74
|
+
if (!auth) return expanded;
|
|
75
|
+
const abs = normalizeSharedSecretsFilePath(expanded);
|
|
76
|
+
if (!abs) return expanded;
|
|
61
77
|
const home = path.normalize(getAifabrixHome());
|
|
62
|
-
if (isPathUnderDir(abs, home)) return
|
|
78
|
+
if (isPathUnderDir(abs, home)) return expanded;
|
|
63
79
|
const work = getAifabrixWork();
|
|
64
80
|
if (work) {
|
|
65
81
|
const w = path.normalize(work);
|
|
66
|
-
if (isPathUnderDir(abs, w)) return
|
|
82
|
+
if (isPathUnderDir(abs, w)) return expanded;
|
|
67
83
|
}
|
|
68
84
|
const base = String(auth.serverUrl).replace(/\/+$/, '');
|
|
69
85
|
return `${base}/api/dev/secrets`;
|
|
@@ -89,7 +89,15 @@ async function getDockerExecEnv() {
|
|
|
89
89
|
if (overlay.DOCKER_HOST && !Object.prototype.hasOwnProperty.call(overlay, 'DOCKER_CERT_PATH')) {
|
|
90
90
|
delete merged.DOCKER_CERT_PATH;
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
let out = { ...merged, ...overlay };
|
|
93
|
+
try {
|
|
94
|
+
const { getBashPrefixedProcessEnvOverlay } = require('./bash-secret-env');
|
|
95
|
+
const bash = await getBashPrefixedProcessEnvOverlay();
|
|
96
|
+
out = { ...out, ...bash };
|
|
97
|
+
} catch {
|
|
98
|
+
/* ignore secrets load failures; Docker CLI still runs with process + remote env */
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
module.exports = { getRemoteDockerEnv, getDockerExecEnv };
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Load shared secrets from Builder Server when aifabrix-secrets is an http(s) URL
|
|
3
|
-
*
|
|
2
|
+
* Load shared secrets from Builder Server when aifabrix-secrets is an http(s) URL,
|
|
3
|
+
* or from the configured shared YAML path when it targets a local file.
|
|
4
4
|
*
|
|
5
5
|
* @fileoverview Remote shared secrets loader for .env generation
|
|
6
6
|
* @author AI Fabrix Team
|
|
7
7
|
* @version 2.0.0
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
10
12
|
const config = require('../core/config');
|
|
13
|
+
const { readYamlAtPath } = require('./secrets-canonical');
|
|
14
|
+
const { ensureSecureFilePermissions } = require('./secure-file-permissions');
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Fetches shared secrets from Builder Server when aifabrix-secrets is an http(s) URL.
|
|
@@ -63,7 +67,42 @@ function mergeUserWithRemoteSecrets(userSecrets, remoteSecrets) {
|
|
|
63
67
|
return merged;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Raw secrets from the configured shared store only (`aifabrix-secrets`): remote Builder API or shared YAML file.
|
|
72
|
+
* Does not include primary user secrets or builder merges. Used to avoid duplicating shared keys into ~/.aifabrix.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Promise<Object|null>} Key-value map or null when unavailable / not configured
|
|
75
|
+
*/
|
|
76
|
+
async function loadConfiguredSharedSecretsStore() {
|
|
77
|
+
const remoteDevAuth = require('./remote-dev-auth');
|
|
78
|
+
const configSecretsPath = await config.getSecretsPath();
|
|
79
|
+
if (!configSecretsPath) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const endpoint = await remoteDevAuth.resolveSharedSecretsEndpoint(configSecretsPath);
|
|
83
|
+
if (remoteDevAuth.isRemoteSecretsUrl(endpoint)) {
|
|
84
|
+
return loadRemoteSharedSecrets();
|
|
85
|
+
}
|
|
86
|
+
const resolvedFile = path.isAbsolute(endpoint)
|
|
87
|
+
? endpoint
|
|
88
|
+
: path.resolve(process.cwd(), endpoint);
|
|
89
|
+
if (!fs.existsSync(resolvedFile)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
ensureSecureFilePermissions(resolvedFile);
|
|
94
|
+
const data = readYamlAtPath(resolvedFile);
|
|
95
|
+
if (!data || typeof data !== 'object') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return data;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
66
104
|
module.exports = {
|
|
67
105
|
loadRemoteSharedSecrets,
|
|
68
|
-
mergeUserWithRemoteSecrets
|
|
106
|
+
mergeUserWithRemoteSecrets,
|
|
107
|
+
loadConfiguredSharedSecretsStore
|
|
69
108
|
};
|
|
@@ -73,16 +73,21 @@ function normalizeDockerRegistryPrefix(registry) {
|
|
|
73
73
|
*/
|
|
74
74
|
function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
|
|
75
75
|
const opts = runOptions || {};
|
|
76
|
+
const tagFromOpts =
|
|
77
|
+
opts.tag !== undefined && opts.tag !== null && String(opts.tag).trim() !== ''
|
|
78
|
+
? String(opts.tag).trim()
|
|
79
|
+
: null;
|
|
80
|
+
|
|
76
81
|
if (opts.image) {
|
|
77
82
|
const parsed = parseImageOverride(opts.image);
|
|
78
83
|
return {
|
|
79
84
|
imageName: parsed ? parsed.name : getRepositoryPathFromConfig(appConfig, appName),
|
|
80
|
-
imageTag: parsed ? parsed.tag : imageTagFromConfig(appConfig)
|
|
85
|
+
imageTag: parsed ? parsed.tag : tagFromOpts || imageTagFromConfig(appConfig)
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
const baseRepo = getRepositoryPathFromConfig(appConfig, appName);
|
|
85
|
-
const imageTag = imageTagFromConfig(appConfig);
|
|
90
|
+
const imageTag = tagFromOpts || imageTagFromConfig(appConfig);
|
|
86
91
|
const prefix =
|
|
87
92
|
normalizeDockerRegistryPrefix(opts.registry) ||
|
|
88
93
|
normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
|
|
@@ -120,5 +125,6 @@ module.exports = {
|
|
|
120
125
|
resolveDockerImageRef,
|
|
121
126
|
resolveComposeImageOverrideString,
|
|
122
127
|
normalizeDockerRegistryPrefix,
|
|
123
|
-
getRepositoryPathFromConfig
|
|
128
|
+
getRepositoryPathFromConfig,
|
|
129
|
+
imageTagFromConfig
|
|
124
130
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect `secrets.local.yaml` paths along the cwd → root walk so parent workspace
|
|
3
|
+
* secrets (e.g. `/workspace/.aifabrix/`) merge when the active config is nested
|
|
4
|
+
* (e.g. `repo/.aifabrix/` from cwd).
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Ancestor secrets.local.yaml discovery for loadSecrets cascade
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const MAX_ANCESTOR_STEPS = 64;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} startDir - Directory to start from (typically process.cwd())
|
|
17
|
+
* @param {(p: string) => boolean} existsSyncFn - Sync existence check
|
|
18
|
+
* @returns {string[]} Absolute paths, nearest ancestor first (cwd-side), then parents
|
|
19
|
+
*/
|
|
20
|
+
function collectAncestorAifabrixSecretsLocalYamlPaths(startDir, existsSyncFn) {
|
|
21
|
+
if (!startDir || typeof startDir !== 'string' || typeof existsSyncFn !== 'function') {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const out = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
let dir = path.resolve(startDir);
|
|
27
|
+
for (let i = 0; i < MAX_ANCESTOR_STEPS; i += 1) {
|
|
28
|
+
const secretsPath = path.join(dir, '.aifabrix', 'secrets.local.yaml');
|
|
29
|
+
if (existsSyncFn(secretsPath)) {
|
|
30
|
+
const abs = path.resolve(secretsPath);
|
|
31
|
+
if (!seen.has(abs)) {
|
|
32
|
+
seen.add(abs);
|
|
33
|
+
out.push(abs);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const parent = path.dirname(dir);
|
|
37
|
+
if (parent === dir) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
collectAncestorAifabrixSecretsLocalYamlPaths
|
|
47
|
+
};
|