@enspirit/emb 0.14.1 → 0.17.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.
- package/README.md +162 -43
- package/dist/src/cli/abstract/BaseCommand.d.ts +1 -0
- package/dist/src/cli/abstract/BaseCommand.js +23 -4
- package/dist/src/cli/abstract/FlavouredCommand.d.ts +1 -0
- package/dist/src/cli/abstract/KubernetesCommand.d.ts +1 -0
- package/dist/src/cli/commands/components/logs.d.ts +2 -1
- package/dist/src/cli/commands/components/logs.js +21 -24
- package/dist/src/cli/commands/secrets/index.d.ts +14 -0
- package/dist/src/cli/commands/secrets/index.js +71 -0
- package/dist/src/cli/commands/secrets/providers.d.ts +12 -0
- package/dist/src/cli/commands/secrets/providers.js +50 -0
- package/dist/src/cli/commands/secrets/validate.d.ts +18 -0
- package/dist/src/cli/commands/secrets/validate.js +145 -0
- package/dist/src/cli/hooks/init.js +7 -1
- package/dist/src/cli/hooks/postrun.d.ts +7 -0
- package/dist/src/cli/hooks/postrun.js +128 -0
- package/dist/src/config/index.d.ts +10 -1
- package/dist/src/config/index.js +28 -3
- package/dist/src/config/schema.d.ts +7 -4
- package/dist/src/config/schema.json +173 -9
- package/dist/src/context.d.ts +9 -0
- package/dist/src/context.js +19 -0
- package/dist/src/docker/compose/operations/ComposeLogsOperation.d.ts +21 -0
- package/dist/src/docker/compose/operations/ComposeLogsOperation.js +85 -0
- package/dist/src/docker/compose/operations/index.d.ts +1 -0
- package/dist/src/docker/compose/operations/index.js +1 -0
- package/dist/src/docker/resources/DockerImageResource.js +16 -6
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/monorepo/monorepo.js +13 -5
- package/dist/src/monorepo/operations/resources/BuildResourcesOperation.js +1 -2
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +1 -1
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +1 -1
- package/dist/src/monorepo/plugins/VaultPlugin.d.ts +46 -0
- package/dist/src/monorepo/plugins/VaultPlugin.js +91 -0
- package/dist/src/monorepo/plugins/index.d.ts +1 -0
- package/dist/src/monorepo/plugins/index.js +3 -0
- package/dist/src/monorepo/resources/index.d.ts +1 -0
- package/dist/src/monorepo/resources/index.js +1 -0
- package/dist/src/secrets/SecretDiscovery.d.ts +46 -0
- package/dist/src/secrets/SecretDiscovery.js +82 -0
- package/dist/src/secrets/SecretManager.d.ts +52 -0
- package/dist/src/secrets/SecretManager.js +75 -0
- package/dist/src/secrets/SecretProvider.d.ts +45 -0
- package/dist/src/secrets/SecretProvider.js +38 -0
- package/dist/src/secrets/index.d.ts +3 -0
- package/dist/src/secrets/index.js +3 -0
- package/dist/src/secrets/providers/VaultOidcHelper.d.ts +39 -0
- package/dist/src/secrets/providers/VaultOidcHelper.js +226 -0
- package/dist/src/secrets/providers/VaultProvider.d.ts +74 -0
- package/dist/src/secrets/providers/VaultProvider.js +266 -0
- package/dist/src/secrets/providers/VaultTokenCache.d.ts +60 -0
- package/dist/src/secrets/providers/VaultTokenCache.js +188 -0
- package/dist/src/secrets/providers/index.d.ts +2 -0
- package/dist/src/secrets/providers/index.js +2 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/src/utils/TemplateExpander.d.ts +13 -1
- package/dist/src/utils/TemplateExpander.js +68 -15
- package/oclif.manifest.json +473 -68
- package/package.json +36 -30
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand, getContext } from '../../index.js';
|
|
3
|
+
import { ComposeLogsOperation } from '../../../docker/index.js';
|
|
3
4
|
export default class ComponentsLogs extends BaseCommand {
|
|
4
5
|
static aliases = ['logs'];
|
|
5
6
|
static description = 'Get components logs.';
|
|
6
7
|
static enableJsonFlag = false;
|
|
7
|
-
static examples = [
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> backend',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> backend frontend',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --no-follow backend',
|
|
13
|
+
];
|
|
14
|
+
static strict = false;
|
|
8
15
|
static flags = {
|
|
9
16
|
follow: Flags.boolean({
|
|
10
17
|
name: 'follow',
|
|
@@ -17,32 +24,22 @@ export default class ComponentsLogs extends BaseCommand {
|
|
|
17
24
|
static args = {
|
|
18
25
|
component: Args.string({
|
|
19
26
|
name: 'component',
|
|
20
|
-
description: 'The component you want to see the logs of',
|
|
21
|
-
required:
|
|
27
|
+
description: 'The component(s) you want to see the logs of (all if omitted)',
|
|
28
|
+
required: false,
|
|
22
29
|
}),
|
|
23
30
|
};
|
|
24
31
|
async run() {
|
|
25
|
-
const { flags,
|
|
26
|
-
const { monorepo
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const { flags, argv } = await this.parse(ComponentsLogs);
|
|
33
|
+
const { monorepo } = await getContext();
|
|
34
|
+
const componentNames = argv;
|
|
35
|
+
// Validate that all specified components exist
|
|
36
|
+
const services = componentNames.map((name) => {
|
|
37
|
+
const component = monorepo.component(name);
|
|
38
|
+
return component.name;
|
|
39
|
+
});
|
|
40
|
+
await monorepo.run(new ComposeLogsOperation(), {
|
|
41
|
+
services: services.length > 0 ? services : undefined,
|
|
42
|
+
follow: flags.follow,
|
|
30
43
|
});
|
|
31
|
-
const container = await docker.getContainer(containerId);
|
|
32
|
-
if (flags.follow) {
|
|
33
|
-
const stream = await container.logs({
|
|
34
|
-
follow: true,
|
|
35
|
-
stderr: true,
|
|
36
|
-
stdout: true,
|
|
37
|
-
});
|
|
38
|
-
docker.modem.demuxStream(stream, process.stdout, process.stderr);
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
const res = await container.logs({
|
|
42
|
-
stderr: true,
|
|
43
|
-
stdout: true,
|
|
44
|
-
});
|
|
45
|
-
this.log(res.toString());
|
|
46
|
-
}
|
|
47
44
|
}
|
|
48
45
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FlavoredCommand } from '../../index.js';
|
|
2
|
+
export interface SecretInfo {
|
|
3
|
+
component?: string;
|
|
4
|
+
key?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
usageCount: number;
|
|
8
|
+
}
|
|
9
|
+
export default class SecretsIndex extends FlavoredCommand<typeof SecretsIndex> {
|
|
10
|
+
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
run(): Promise<SecretInfo[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { printTable } from '@oclif/table';
|
|
3
|
+
import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js';
|
|
4
|
+
import { aggregateSecrets, discoverSecrets, } from '../../../secrets/SecretDiscovery.js';
|
|
5
|
+
export default class SecretsIndex extends FlavoredCommand {
|
|
6
|
+
static description = 'List all secret references in the configuration.';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
11
|
+
];
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(SecretsIndex);
|
|
14
|
+
const context = getContext();
|
|
15
|
+
const { monorepo, secrets } = context;
|
|
16
|
+
// Get registered secret providers dynamically
|
|
17
|
+
const secretProviders = new Set(secrets.getProviderNames());
|
|
18
|
+
// Collect secrets from all configuration sources
|
|
19
|
+
const allSecrets = [];
|
|
20
|
+
// Scan monorepo-level config (env, vars, tasks, defaults, flavors)
|
|
21
|
+
allSecrets.push(...discoverSecrets({
|
|
22
|
+
env: monorepo.config.env,
|
|
23
|
+
vars: monorepo.config.vars,
|
|
24
|
+
tasks: monorepo.config.tasks,
|
|
25
|
+
defaults: monorepo.config.defaults,
|
|
26
|
+
flavors: monorepo.config.flavors,
|
|
27
|
+
}, { file: '.emb.yml' }, secretProviders));
|
|
28
|
+
// Scan each component's config
|
|
29
|
+
for (const component of monorepo.components) {
|
|
30
|
+
allSecrets.push(...discoverSecrets({
|
|
31
|
+
tasks: component.config.tasks,
|
|
32
|
+
resources: component.config.resources,
|
|
33
|
+
}, {
|
|
34
|
+
file: `${component.name}/Embfile.yml`,
|
|
35
|
+
component: component.name,
|
|
36
|
+
}, secretProviders));
|
|
37
|
+
}
|
|
38
|
+
// Aggregate by unique secret reference
|
|
39
|
+
const aggregated = aggregateSecrets(allSecrets);
|
|
40
|
+
// Convert to output format
|
|
41
|
+
const result = aggregated.map((secret) => ({
|
|
42
|
+
provider: secret.provider,
|
|
43
|
+
path: secret.path,
|
|
44
|
+
key: secret.key,
|
|
45
|
+
component: secret.locations
|
|
46
|
+
.map((l) => l.component)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(', ') || undefined,
|
|
49
|
+
usageCount: secret.locations.length,
|
|
50
|
+
}));
|
|
51
|
+
if (!flags.json) {
|
|
52
|
+
if (result.length === 0) {
|
|
53
|
+
this.log('No secret references found in configuration.');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
printTable({
|
|
57
|
+
...TABLE_DEFAULTS,
|
|
58
|
+
columns: ['provider', 'path', 'key', 'component', 'usageCount'],
|
|
59
|
+
data: result.map((r) => ({
|
|
60
|
+
...r,
|
|
61
|
+
key: r.key || '-',
|
|
62
|
+
component: r.component || '-',
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
const providerCount = new Set(result.map((r) => r.provider)).size;
|
|
66
|
+
this.log(`\nFound ${result.length} secret reference(s) using ${providerCount} provider(s).`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FlavoredCommand } from '../../index.js';
|
|
2
|
+
export interface ProviderInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
status: 'connected' | 'not_configured';
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
export default class SecretsProviders extends FlavoredCommand<typeof SecretsProviders> {
|
|
8
|
+
static description: string;
|
|
9
|
+
static enableJsonFlag: boolean;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
run(): Promise<ProviderInfo[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { printTable } from '@oclif/table';
|
|
3
|
+
import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js';
|
|
4
|
+
export default class SecretsProviders extends FlavoredCommand {
|
|
5
|
+
static description = 'Show configured secret providers and their status.';
|
|
6
|
+
static enableJsonFlag = true;
|
|
7
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(SecretsProviders);
|
|
10
|
+
const context = getContext();
|
|
11
|
+
const { secrets } = context;
|
|
12
|
+
const providerNames = secrets.getProviderNames();
|
|
13
|
+
if (providerNames.length === 0) {
|
|
14
|
+
if (!flags.json) {
|
|
15
|
+
this.log('No secret providers configured.');
|
|
16
|
+
this.log('\nTo configure a provider, add it to your .emb.yml:');
|
|
17
|
+
this.log(`
|
|
18
|
+
plugins:
|
|
19
|
+
- name: vault
|
|
20
|
+
config:
|
|
21
|
+
address: https://vault.example.com
|
|
22
|
+
auth:
|
|
23
|
+
method: oidc
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const results = providerNames.map((name) => {
|
|
29
|
+
const provider = secrets.get(name);
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
type: provider?.constructor.name || 'Unknown',
|
|
33
|
+
status: provider ? 'connected' : 'not_configured',
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
if (!flags.json) {
|
|
37
|
+
printTable({
|
|
38
|
+
...TABLE_DEFAULTS,
|
|
39
|
+
columns: ['name', 'type', 'status'],
|
|
40
|
+
data: results.map((r) => ({
|
|
41
|
+
name: r.name,
|
|
42
|
+
type: r.type,
|
|
43
|
+
status: r.status === 'connected' ? '✔ Connected' : '✖ Not configured',
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
this.log(`\n${results.length} provider(s) configured.`);
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FlavoredCommand } from '../../index.js';
|
|
2
|
+
export interface ValidationResult {
|
|
3
|
+
error?: string;
|
|
4
|
+
key?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
status: 'error' | 'ok';
|
|
8
|
+
}
|
|
9
|
+
export default class SecretsValidate extends FlavoredCommand<typeof SecretsValidate> {
|
|
10
|
+
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
static flags: {
|
|
14
|
+
'fail-fast': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<ValidationResult[]>;
|
|
17
|
+
private validateSecret;
|
|
18
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { printTable } from '@oclif/table';
|
|
4
|
+
import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js';
|
|
5
|
+
import { aggregateSecrets, discoverSecrets, } from '../../../secrets/SecretDiscovery.js';
|
|
6
|
+
export default class SecretsValidate extends FlavoredCommand {
|
|
7
|
+
static description = 'Validate that all secret references can be resolved (without showing values).';
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --fail-fast',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
'fail-fast': Flags.boolean({
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'Stop on first validation error',
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(SecretsValidate);
|
|
22
|
+
const context = getContext();
|
|
23
|
+
const { monorepo, secrets } = context;
|
|
24
|
+
// Get registered secret providers dynamically
|
|
25
|
+
const secretProviders = new Set(secrets.getProviderNames());
|
|
26
|
+
// Collect secrets from all configuration sources
|
|
27
|
+
const allSecrets = [];
|
|
28
|
+
// Scan monorepo-level config (env, vars, tasks, defaults, flavors)
|
|
29
|
+
allSecrets.push(...discoverSecrets({
|
|
30
|
+
env: monorepo.config.env,
|
|
31
|
+
vars: monorepo.config.vars,
|
|
32
|
+
tasks: monorepo.config.tasks,
|
|
33
|
+
defaults: monorepo.config.defaults,
|
|
34
|
+
flavors: monorepo.config.flavors,
|
|
35
|
+
}, { file: '.emb.yml' }, secretProviders));
|
|
36
|
+
// Scan each component's config
|
|
37
|
+
for (const component of monorepo.components) {
|
|
38
|
+
allSecrets.push(...discoverSecrets({
|
|
39
|
+
tasks: component.config.tasks,
|
|
40
|
+
resources: component.config.resources,
|
|
41
|
+
}, {
|
|
42
|
+
file: `${component.name}/Embfile.yml`,
|
|
43
|
+
component: component.name,
|
|
44
|
+
}, secretProviders));
|
|
45
|
+
}
|
|
46
|
+
// Aggregate by unique secret reference
|
|
47
|
+
const aggregated = aggregateSecrets(allSecrets);
|
|
48
|
+
if (aggregated.length === 0) {
|
|
49
|
+
if (!flags.json) {
|
|
50
|
+
this.log('No secret references found in configuration.');
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
// Validate each secret
|
|
55
|
+
const results = [];
|
|
56
|
+
let hasErrors = false;
|
|
57
|
+
for (const secret of aggregated) {
|
|
58
|
+
// Sequential validation is intentional for fail-fast support
|
|
59
|
+
// eslint-disable-next-line no-await-in-loop
|
|
60
|
+
const result = await this.validateSecret(secret, secrets);
|
|
61
|
+
results.push(result);
|
|
62
|
+
if (result.status === 'error') {
|
|
63
|
+
hasErrors = true;
|
|
64
|
+
if (flags['fail-fast']) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!flags.json) {
|
|
70
|
+
printTable({
|
|
71
|
+
...TABLE_DEFAULTS,
|
|
72
|
+
columns: ['status', 'provider', 'path', 'key'],
|
|
73
|
+
data: results.map((r) => ({
|
|
74
|
+
status: r.status === 'ok' ? '✔' : '✖',
|
|
75
|
+
provider: r.provider,
|
|
76
|
+
path: r.path,
|
|
77
|
+
key: r.key || '-',
|
|
78
|
+
})),
|
|
79
|
+
});
|
|
80
|
+
const passed = results.filter((r) => r.status === 'ok').length;
|
|
81
|
+
const failed = results.filter((r) => r.status === 'error').length;
|
|
82
|
+
this.log(`\nValidation: ${passed} passed, ${failed} failed`);
|
|
83
|
+
// Show error details
|
|
84
|
+
const errors = results.filter((r) => r.status === 'error');
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
this.log('\nError details:');
|
|
87
|
+
for (const error of errors) {
|
|
88
|
+
const ref = error.key
|
|
89
|
+
? `${error.provider}:${error.path}#${error.key}`
|
|
90
|
+
: `${error.provider}:${error.path}`;
|
|
91
|
+
this.log(` - ${ref}: ${error.error}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Exit with error code if validation failed
|
|
96
|
+
if (hasErrors) {
|
|
97
|
+
this.exit(1);
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
async validateSecret(secret, secrets) {
|
|
102
|
+
const provider = secrets.get(secret.provider);
|
|
103
|
+
if (!provider) {
|
|
104
|
+
return {
|
|
105
|
+
provider: secret.provider,
|
|
106
|
+
path: secret.path,
|
|
107
|
+
key: secret.key,
|
|
108
|
+
status: 'error',
|
|
109
|
+
error: `Provider '${secret.provider}' not configured`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
// Actually fetch the secret to verify access
|
|
114
|
+
const result = await provider.get({
|
|
115
|
+
path: secret.path,
|
|
116
|
+
key: secret.key,
|
|
117
|
+
});
|
|
118
|
+
// If a key was specified, verify it exists
|
|
119
|
+
if (secret.key && result === undefined) {
|
|
120
|
+
return {
|
|
121
|
+
provider: secret.provider,
|
|
122
|
+
path: secret.path,
|
|
123
|
+
key: secret.key,
|
|
124
|
+
status: 'error',
|
|
125
|
+
error: `Key '${secret.key}' not found in secret`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
provider: secret.provider,
|
|
130
|
+
path: secret.path,
|
|
131
|
+
key: secret.key,
|
|
132
|
+
status: 'ok',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
provider: secret.provider,
|
|
138
|
+
path: secret.path,
|
|
139
|
+
key: secret.key,
|
|
140
|
+
status: 'error',
|
|
141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { settings } from '@oclif/core';
|
|
2
|
+
const hook = async function (_options) {
|
|
3
|
+
// Disable oclif's auto-transpilation to avoid spurious warnings when npm-linked.
|
|
4
|
+
// We always run from compiled JS in dist/, so auto-transpilation is not needed.
|
|
5
|
+
// This prevents a double tsPath() call that produces "Could not find source" warnings.
|
|
6
|
+
settings.enableAutoTranspile = false;
|
|
7
|
+
};
|
|
2
8
|
export default hook;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Task completion function to inject into the bash completion script.
|
|
6
|
+
* This function is called when completing arguments for `tasks run` or `run`.
|
|
7
|
+
*/
|
|
8
|
+
const TASK_COMPLETION_FUNCTION = `
|
|
9
|
+
# EMB: Task name completion for 'tasks run' and 'run' commands
|
|
10
|
+
_emb_complete_tasks() {
|
|
11
|
+
local cur="\${1:-}"
|
|
12
|
+
local tasks
|
|
13
|
+
|
|
14
|
+
# Get task IDs from emb tasks --json, extract the id field
|
|
15
|
+
tasks=$(emb tasks --json 2>/dev/null | grep -o '"id": *"[^"]*"' | sed 's/"id": *"//g' | sed 's/"//g')
|
|
16
|
+
|
|
17
|
+
if [[ -z "$tasks" ]]; then
|
|
18
|
+
return
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Filter by substring match if provided (more flexible than prefix-only)
|
|
22
|
+
if [[ -n "$cur" ]]; then
|
|
23
|
+
local matches=""
|
|
24
|
+
for task in $tasks; do
|
|
25
|
+
if [[ "$task" == *"$cur"* ]]; then
|
|
26
|
+
matches="$matches $task"
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
COMPREPLY=($matches)
|
|
30
|
+
else
|
|
31
|
+
COMPREPLY=($tasks)
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
/**
|
|
36
|
+
* Enhanced completion logic to inject after normalizedCommand is calculated.
|
|
37
|
+
* Checks if we're completing task arguments for 'tasks run' or 'run'.
|
|
38
|
+
*/
|
|
39
|
+
const TASK_COMPLETION_LOGIC = `
|
|
40
|
+
# EMB: Check if we're completing task names for 'tasks run' or 'run'
|
|
41
|
+
if [[ "$normalizedCommand" == tasks:run:* ]] || [[ "$normalizedCommand" == run:* ]] || [[ "$normalizedCommand" == "tasks:run" ]] || [[ "$normalizedCommand" == "run" ]]; then
|
|
42
|
+
_emb_complete_tasks "$cur"
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
`;
|
|
46
|
+
/**
|
|
47
|
+
* Zsh task completion function to inject.
|
|
48
|
+
* Uses zsh's compadd to add task completions.
|
|
49
|
+
* Note: We use compadd instead of _describe because task IDs contain colons
|
|
50
|
+
* (e.g., "frontend:test") and _describe uses colons as value:description delimiter.
|
|
51
|
+
*/
|
|
52
|
+
const ZSH_TASK_COMPLETION_FUNCTION = `
|
|
53
|
+
# EMB: Task name completion for 'tasks run' and 'run' commands
|
|
54
|
+
_emb_complete_tasks() {
|
|
55
|
+
local tasks
|
|
56
|
+
# Get task IDs from emb tasks --json, extract the id field
|
|
57
|
+
tasks=(\${(f)"$(emb tasks --json 2>/dev/null | grep -o '"id": *"[^"]*"' | sed 's/"id": *"//g' | sed 's/"//g')"})
|
|
58
|
+
|
|
59
|
+
if [[ \${#tasks[@]} -gt 0 ]]; then
|
|
60
|
+
# Use compadd instead of _describe because task IDs contain colons
|
|
61
|
+
# which _describe interprets as value:description separators
|
|
62
|
+
compadd -a tasks
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
/**
|
|
67
|
+
* Patches the bash completion script.
|
|
68
|
+
*/
|
|
69
|
+
function patchBashCompletion() {
|
|
70
|
+
const possiblePaths = [
|
|
71
|
+
join(homedir(), 'Library/Caches/emb/autocomplete/functions/bash/emb.bash'),
|
|
72
|
+
join(homedir(), '.cache/emb/autocomplete/functions/bash/emb.bash'),
|
|
73
|
+
];
|
|
74
|
+
const scriptPath = possiblePaths.find((p) => existsSync(p));
|
|
75
|
+
if (!scriptPath) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const content = readFileSync(scriptPath, 'utf8');
|
|
79
|
+
// Check if already patched
|
|
80
|
+
if (content.includes('_emb_complete_tasks')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Insert task completion function after the join_by function
|
|
84
|
+
let patched = content.replace(/function join_by \{[^}]+\}/, (match) => `${match}\n${TASK_COMPLETION_FUNCTION}`);
|
|
85
|
+
// Insert task completion logic right before the "if [[ -z "$normalizedCommand" ]]" check
|
|
86
|
+
patched = patched.replace('if [[ -z "$normalizedCommand" ]]; then', `${TASK_COMPLETION_LOGIC.trim()}\n\n if [[ -z "$normalizedCommand" ]]; then`);
|
|
87
|
+
writeFileSync(scriptPath, patched, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Patches the zsh completion script.
|
|
91
|
+
*/
|
|
92
|
+
function patchZshCompletion() {
|
|
93
|
+
const possiblePaths = [
|
|
94
|
+
join(homedir(), 'Library/Caches/emb/autocomplete/functions/zsh/_emb'),
|
|
95
|
+
join(homedir(), '.cache/emb/autocomplete/functions/zsh/_emb'),
|
|
96
|
+
];
|
|
97
|
+
const scriptPath = possiblePaths.find((p) => existsSync(p));
|
|
98
|
+
if (!scriptPath) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const content = readFileSync(scriptPath, 'utf8');
|
|
102
|
+
// Check if already patched
|
|
103
|
+
if (content.includes('_emb_complete_tasks')) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Insert task completion function after #compdef line
|
|
107
|
+
let patched = content.replace('#compdef emb', `#compdef emb\n${ZSH_TASK_COMPLETION_FUNCTION}`);
|
|
108
|
+
// Replace _files with _emb_complete_tasks in run command sections
|
|
109
|
+
// Pattern 1: Inside _emb_tasks for "tasks run"
|
|
110
|
+
patched = patched.replace(/("run"\)\s*\n\s*_arguments -S[\s\S]*?)"?\*: :_files"(\s*;;)/, '$1"*: :_emb_complete_tasks"$2');
|
|
111
|
+
// Pattern 2: Top-level run) command (the alias)
|
|
112
|
+
patched = patched.replace(/(^run\)\n_arguments -S[\s\S]*?)"?\*: :_files"(\s*;;)/m, '$1"*: :_emb_complete_tasks"$2');
|
|
113
|
+
writeFileSync(scriptPath, patched, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Postrun hook that patches completion scripts after autocomplete generation.
|
|
117
|
+
* This adds task name completion for the 'tasks run' and 'run' commands.
|
|
118
|
+
*/
|
|
119
|
+
const hook = async function (options) {
|
|
120
|
+
const commandId = options.Command?.id;
|
|
121
|
+
// Only patch after autocomplete commands
|
|
122
|
+
if (commandId !== 'autocomplete' && commandId !== 'autocomplete:create') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
patchBashCompletion();
|
|
126
|
+
patchZshCompletion();
|
|
127
|
+
};
|
|
128
|
+
export default hook;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export * from './validation.js';
|
|
3
|
-
export
|
|
3
|
+
export interface LoadConfigOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Explicit root directory path. Takes precedence over EMB_ROOT env var.
|
|
6
|
+
* Can be either:
|
|
7
|
+
* - A directory containing .emb.yml
|
|
8
|
+
* - A direct path to a .emb.yml file
|
|
9
|
+
*/
|
|
10
|
+
root?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const loadConfig: (options?: LoadConfigOptions) => Promise<{
|
|
4
13
|
rootDir: string;
|
|
5
14
|
config: import("./schema.js").EMBConfig;
|
|
6
15
|
}>;
|
package/dist/src/config/index.js
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import { findUp } from 'find-up';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { validateUserConfig } from './validation.js';
|
|
4
5
|
export * from './types.js';
|
|
5
6
|
export * from './validation.js';
|
|
6
|
-
export const loadConfig = async () => {
|
|
7
|
-
|
|
7
|
+
export const loadConfig = async (options = {}) => {
|
|
8
|
+
let path;
|
|
9
|
+
// Priority 1: Explicit root option (from --root/-C flag)
|
|
10
|
+
// Priority 2: EMB_ROOT environment variable
|
|
11
|
+
// Priority 3: Walk up to find .emb.yml (original behavior)
|
|
12
|
+
const explicitRoot = options.root || process.env.EMB_ROOT;
|
|
13
|
+
if (explicitRoot) {
|
|
14
|
+
const resolved = resolve(explicitRoot);
|
|
15
|
+
// Check if it's a direct path to a config file
|
|
16
|
+
if (resolved.endsWith('.emb.yml') && existsSync(resolved)) {
|
|
17
|
+
path = resolved;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Assume it's a directory, look for .emb.yml inside
|
|
21
|
+
const configPath = join(resolved, '.emb.yml');
|
|
22
|
+
if (existsSync(configPath)) {
|
|
23
|
+
path = configPath;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error(`Could not find .emb.yml in specified root: ${explicitRoot}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
path = await findUp('.emb.yml');
|
|
32
|
+
}
|
|
8
33
|
if (!path) {
|
|
9
34
|
throw new Error('Could not find EMB config anywhere');
|
|
10
35
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
|
4
4
|
* and run json-schema-to-typescript to regenerate this file.
|
|
5
5
|
*/
|
|
6
|
+
export type PluginConfigItem = {
|
|
7
|
+
[k: string]: unknown;
|
|
8
|
+
} & {
|
|
9
|
+
name: Identifier;
|
|
10
|
+
config?: unknown;
|
|
11
|
+
};
|
|
6
12
|
export type Identifier = string;
|
|
7
13
|
export type TaskConfig = TaskConfig1 & {
|
|
8
14
|
description?: string;
|
|
@@ -55,10 +61,7 @@ export interface EMBConfig {
|
|
|
55
61
|
*/
|
|
56
62
|
rootDir?: string;
|
|
57
63
|
};
|
|
58
|
-
plugins?:
|
|
59
|
-
name: Identifier;
|
|
60
|
-
config?: unknown;
|
|
61
|
-
}[];
|
|
64
|
+
plugins?: PluginConfigItem[];
|
|
62
65
|
/**
|
|
63
66
|
* Variables to install on the environment
|
|
64
67
|
*/
|