@outputai/cli 0.1.13-dev.01b8dea.0 → 0.1.13-dev.98dfd72.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.
@@ -81,7 +81,7 @@ services:
81
81
  condition: service_healthy
82
82
  worker:
83
83
  condition: service_healthy
84
- image: outputai/api:${OUTPUT_API_VERSION:-0.1.13-dev.01b8dea.0}
84
+ image: outputai/api:${OUTPUT_API_VERSION:-0.1.13-dev.98dfd72.0}
85
85
  init: true
86
86
  networks:
87
87
  - main
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CredentialsSet extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ workflow: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,57 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { load as parseYaml, dump as stringifyYaml } from 'js-yaml';
3
+ import { decryptCredentials, credentialsExist, writeEncrypted, resolveCredentialsPath } from '#services/credentials_service.js';
4
+ const setNestedValue = (obj, dotPath, value) => {
5
+ const parts = dotPath.split('.');
6
+ const parent = parts.slice(0, -1).reduce((current, key) => {
7
+ if (!current[key] || typeof current[key] !== 'object') {
8
+ current[key] = {};
9
+ }
10
+ return current[key];
11
+ }, obj);
12
+ parent[parts[parts.length - 1]] = value;
13
+ };
14
+ export default class CredentialsSet extends Command {
15
+ static description = 'Set a credential value by dot-notation path';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> anthropic.api_key sk-ant-...',
18
+ '<%= config.bin %> <%= command.id %> openai.api_key sk-... --environment production',
19
+ '<%= config.bin %> <%= command.id %> stripe.key sk_live_... --workflow my_workflow'
20
+ ];
21
+ static args = {
22
+ path: Args.string({
23
+ description: 'Dot-notation path to the credential (e.g. anthropic.api_key)',
24
+ required: true
25
+ }),
26
+ value: Args.string({
27
+ description: 'Value to set',
28
+ required: true
29
+ })
30
+ };
31
+ static flags = {
32
+ environment: Flags.string({
33
+ char: 'e',
34
+ description: 'Target environment (e.g. production, development)'
35
+ }),
36
+ workflow: Flags.string({
37
+ char: 'w',
38
+ description: 'Target a specific workflow directory'
39
+ })
40
+ };
41
+ async run() {
42
+ const { args, flags } = await this.parse(CredentialsSet);
43
+ const environment = flags.environment;
44
+ const workflow = flags.workflow;
45
+ if (environment && workflow) {
46
+ this.error('Cannot specify both --environment and --workflow.');
47
+ }
48
+ if (!credentialsExist(environment, workflow)) {
49
+ this.error(`No credentials file found at ${resolveCredentialsPath(environment, workflow)}. Run "output credentials init" first.`);
50
+ }
51
+ const plaintext = decryptCredentials(environment, workflow);
52
+ const data = (parseYaml(plaintext) || {});
53
+ setNestedValue(data, args.path, args.value);
54
+ writeEncrypted(environment, stringifyYaml(data), workflow);
55
+ this.log(`Set ${args.path}`);
56
+ }
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
+ import * as credentialsService from '#services/credentials_service.js';
4
+ import CredentialsSet from './set.js';
5
+ vi.mock('#services/credentials_service.js');
6
+ vi.mock('js-yaml', () => ({
7
+ load: vi.fn((yaml) => {
8
+ if (yaml.includes('sk-existing')) {
9
+ return { anthropic: { api_key: 'sk-existing' } };
10
+ }
11
+ return {};
12
+ }),
13
+ dump: vi.fn((obj) => JSON.stringify(obj))
14
+ }));
15
+ describe('credentials set command', () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ vi.mocked(credentialsService.credentialsExist).mockReturnValue(true);
19
+ vi.mocked(credentialsService.decryptCredentials).mockReturnValue('anthropic:\n api_key: sk-existing\n');
20
+ vi.mocked(credentialsService.writeEncrypted).mockImplementation(() => { });
21
+ });
22
+ afterEach(() => {
23
+ vi.restoreAllMocks();
24
+ });
25
+ const createTestCommand = (parsedArgs = {}, flags = {}) => {
26
+ const cmd = new CredentialsSet([], {});
27
+ cmd.log = vi.fn();
28
+ cmd.error = vi.fn((msg) => {
29
+ throw new Error(msg);
30
+ });
31
+ Object.defineProperty(cmd, 'parse', {
32
+ value: vi.fn().mockResolvedValue({
33
+ args: { path: 'anthropic.api_key', value: 'sk-new-key', ...parsedArgs },
34
+ flags: { environment: undefined, workflow: undefined, ...flags }
35
+ }),
36
+ configurable: true
37
+ });
38
+ return cmd;
39
+ };
40
+ describe('command structure', () => {
41
+ it('should have correct description', () => {
42
+ expect(CredentialsSet.description).toContain('credential value');
43
+ });
44
+ it('should have required path and value arguments', () => {
45
+ expect(CredentialsSet.args.path).toBeDefined();
46
+ expect(CredentialsSet.args.path.required).toBe(true);
47
+ expect(CredentialsSet.args.value).toBeDefined();
48
+ expect(CredentialsSet.args.value.required).toBe(true);
49
+ });
50
+ it('should have environment and workflow flags', () => {
51
+ expect(CredentialsSet.flags.environment).toBeDefined();
52
+ expect(CredentialsSet.flags.workflow).toBeDefined();
53
+ });
54
+ });
55
+ describe('command execution', () => {
56
+ it('should decrypt, update, and re-encrypt credentials', async () => {
57
+ const cmd = createTestCommand();
58
+ await cmd.run();
59
+ expect(credentialsService.decryptCredentials).toHaveBeenCalledWith(undefined, undefined);
60
+ expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, expect.any(String), undefined);
61
+ expect(cmd.log).toHaveBeenCalledWith('Set anthropic.api_key');
62
+ });
63
+ it('should create nested keys that do not exist', async () => {
64
+ vi.mocked(credentialsService.decryptCredentials).mockReturnValue('');
65
+ const cmd = createTestCommand({ path: 'new.nested.key', value: 'my-value' });
66
+ await cmd.run();
67
+ expect(credentialsService.writeEncrypted).toHaveBeenCalledTimes(1);
68
+ expect(cmd.log).toHaveBeenCalledWith('Set new.nested.key');
69
+ });
70
+ it('should pass environment flag to service functions', async () => {
71
+ const cmd = createTestCommand({}, { environment: 'production' });
72
+ await cmd.run();
73
+ expect(credentialsService.credentialsExist).toHaveBeenCalledWith('production', undefined);
74
+ expect(credentialsService.decryptCredentials).toHaveBeenCalledWith('production', undefined);
75
+ expect(credentialsService.writeEncrypted).toHaveBeenCalledWith('production', expect.any(String), undefined);
76
+ });
77
+ it('should pass workflow flag to service functions', async () => {
78
+ const cmd = createTestCommand({}, { workflow: 'my_workflow' });
79
+ await cmd.run();
80
+ expect(credentialsService.credentialsExist).toHaveBeenCalledWith(undefined, 'my_workflow');
81
+ expect(credentialsService.decryptCredentials).toHaveBeenCalledWith(undefined, 'my_workflow');
82
+ expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, expect.any(String), 'my_workflow');
83
+ });
84
+ it('should error when both environment and workflow are specified', async () => {
85
+ const cmd = createTestCommand({}, { environment: 'production', workflow: 'my_workflow' });
86
+ await expect(cmd.run()).rejects.toThrow('Cannot specify both');
87
+ });
88
+ it('should error when credentials file does not exist', async () => {
89
+ vi.mocked(credentialsService.credentialsExist).mockReturnValue(false);
90
+ vi.mocked(credentialsService.resolveCredentialsPath).mockReturnValue('/project/config/credentials.yml.enc');
91
+ const cmd = createTestCommand();
92
+ await expect(cmd.run()).rejects.toThrow('No credentials file found');
93
+ });
94
+ });
95
+ });
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.1.13-dev.01b8dea.0"
2
+ "framework": "0.1.13-dev.98dfd72.0"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.1.13-dev.01b8dea.0",
3
+ "version": "0.1.13-dev.98dfd72.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,9 +36,9 @@
36
36
  "semver": "7.7.4",
37
37
  "undici": "8.0.2",
38
38
  "yaml": "^2.8.3",
39
- "@outputai/credentials": "0.1.13-dev.01b8dea.0",
40
- "@outputai/evals": "0.1.13-dev.01b8dea.0",
41
- "@outputai/llm": "0.1.13-dev.01b8dea.0"
39
+ "@outputai/credentials": "0.1.13-dev.98dfd72.0",
40
+ "@outputai/evals": "0.1.13-dev.98dfd72.0",
41
+ "@outputai/llm": "0.1.13-dev.98dfd72.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/cli-progress": "3.11.6",