@outputai/cli 0.6.1-next.83742db.0 → 0.6.1-next.f67f4b9.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.
@@ -80,7 +80,7 @@ services:
80
80
  condition: service_healthy
81
81
  worker:
82
82
  condition: service_healthy
83
- image: outputai/api:${OUTPUT_API_VERSION:-0.6.1-next.83742db.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.6.1-next.f67f4b9.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -5,7 +5,6 @@ export default class CredentialsEdit extends Command {
5
5
  static flags: {
6
6
  environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  workflow: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
- force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
8
  };
10
9
  run(): Promise<void>;
11
10
  }
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { load as parseYaml } from 'js-yaml';
7
- import { decryptCredentials, writeEncrypted, credentialsExist, resolveCredentialsPath, checkKeyMatchesCredentials, reEncryptKeyMismatchMessage } from '#services/credentials_service.js';
7
+ import { decryptCredentials, writeEncrypted, credentialsExist, resolveCredentialsPath } from '#services/credentials_service.js';
8
8
  export default class CredentialsEdit extends Command {
9
9
  static description = 'Edit encrypted credentials in your $EDITOR';
10
10
  static examples = [
@@ -20,11 +20,6 @@ export default class CredentialsEdit extends Command {
20
20
  workflow: Flags.string({
21
21
  char: 'w',
22
22
  description: 'Target a specific workflow directory'
23
- }),
24
- force: Flags.boolean({
25
- char: 'f',
26
- description: 'Edit on an empty file and re-encrypt with the current key when it cannot decrypt the existing file (discards existing values)',
27
- default: false
28
23
  })
29
24
  };
30
25
  async run() {
@@ -37,16 +32,9 @@ export default class CredentialsEdit extends Command {
37
32
  if (!credentialsExist(environment, workflow)) {
38
33
  this.error(`No credentials file found at ${resolveCredentialsPath(environment, workflow)}. Run "output credentials init" first.`);
39
34
  }
40
- const keyMismatch = checkKeyMatchesCredentials(environment, workflow) === 'mismatch';
41
- if (keyMismatch && !flags.force) {
42
- this.error(reEncryptKeyMismatchMessage(resolveCredentialsPath(environment, workflow)));
43
- }
44
- if (keyMismatch) {
45
- this.warn(reEncryptKeyMismatchMessage(resolveCredentialsPath(environment, workflow)));
46
- }
47
35
  const editorEnv = process.env.EDITOR || process.env.VISUAL || 'vi';
48
36
  const [editorCmd, ...editorArgs] = editorEnv.split(/\s+/);
49
- const plaintext = keyMismatch ? '' : decryptCredentials(environment, workflow);
37
+ const plaintext = decryptCredentials(environment, workflow);
50
38
  const tmpFile = path.join(os.tmpdir(), `output-credentials-${Date.now()}.yml`);
51
39
  try {
52
40
  fs.writeFileSync(tmpFile, plaintext, { mode: 0o600 });
@@ -23,8 +23,6 @@ describe('credentials edit command', () => {
23
23
  beforeEach(() => {
24
24
  vi.clearAllMocks();
25
25
  vi.mocked(credentialsService.credentialsExist).mockReturnValue(true);
26
- vi.mocked(credentialsService.checkKeyMatchesCredentials).mockReturnValue('match');
27
- vi.mocked(credentialsService.reEncryptKeyMismatchMessage).mockReturnValue('KEY_MISMATCH_WARNING');
28
26
  vi.mocked(credentialsService.decryptCredentials).mockReturnValue('anthropic:\n api_key: sk-test\n');
29
27
  });
30
28
  afterEach(() => {
@@ -33,14 +31,13 @@ describe('credentials edit command', () => {
33
31
  const createTestCommand = (flags = {}) => {
34
32
  const cmd = new CredentialsEdit([], {});
35
33
  cmd.log = vi.fn();
36
- cmd.warn = vi.fn();
37
34
  cmd.error = vi.fn((msg) => {
38
35
  throw new Error(msg);
39
36
  });
40
37
  Object.defineProperty(cmd, 'parse', {
41
38
  value: vi.fn().mockResolvedValue({
42
39
  args: {},
43
- flags: { environment: undefined, workflow: undefined, force: false, ...flags }
40
+ flags: { environment: undefined, workflow: undefined, ...flags }
44
41
  }),
45
42
  configurable: true
46
43
  });
@@ -50,10 +47,9 @@ describe('credentials edit command', () => {
50
47
  it('should have correct description', () => {
51
48
  expect(CredentialsEdit.description).toContain('Edit');
52
49
  });
53
- it('should have environment, workflow, and force flags', () => {
50
+ it('should have environment and workflow flags', () => {
54
51
  expect(CredentialsEdit.flags.environment).toBeDefined();
55
52
  expect(CredentialsEdit.flags.workflow).toBeDefined();
56
- expect(CredentialsEdit.flags.force).toBeDefined();
57
53
  });
58
54
  });
59
55
  describe('command execution', () => {
@@ -75,24 +71,6 @@ describe('credentials edit command', () => {
75
71
  await expect(cmd.run()).rejects.toThrow('No credentials file found');
76
72
  });
77
73
  });
78
- describe('key mismatch handling', () => {
79
- it('should error and not decrypt when the key cannot decrypt and --force is absent', async () => {
80
- vi.mocked(credentialsService.checkKeyMatchesCredentials).mockReturnValue('mismatch');
81
- const cmd = createTestCommand();
82
- await expect(cmd.run()).rejects.toThrow('KEY_MISMATCH_WARNING');
83
- expect(credentialsService.decryptCredentials).not.toHaveBeenCalled();
84
- expect(credentialsService.writeEncrypted).not.toHaveBeenCalled();
85
- });
86
- it('should warn, edit from empty, and re-encrypt when --force is passed', async () => {
87
- vi.mocked(credentialsService.checkKeyMatchesCredentials).mockReturnValue('mismatch');
88
- vi.mocked(fs.readFileSync).mockReturnValue('edited: content\n');
89
- const cmd = createTestCommand({ force: true });
90
- await cmd.run();
91
- expect(cmd.warn).toHaveBeenCalledWith('KEY_MISMATCH_WARNING');
92
- expect(credentialsService.decryptCredentials).not.toHaveBeenCalled();
93
- expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, 'edited: content\n', undefined);
94
- });
95
- });
96
74
  // Regression: OUT-441 — editing without making changes must not re-encrypt
97
75
  // the file. AES-GCM uses a fresh nonce per encrypt, so an unconditional
98
76
  // re-write produces a new ciphertext and leaves the file dirty in git.
@@ -10,7 +10,6 @@ export default class CredentialsSet extends Command {
10
10
  environment: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  workflow: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
13
  };
15
14
  private confirmOverwrite;
16
15
  run(): Promise<void>;
@@ -2,7 +2,7 @@ import { Args, Command, Flags } from '@oclif/core';
2
2
  import { confirm } from '@inquirer/prompts';
3
3
  import { load as parseYaml, dump as stringifyYaml } from 'js-yaml';
4
4
  import { getErrorMessage } from '#utils/error_utils.js';
5
- import { decryptCredentials, credentialsExist, writeEncrypted, resolveCredentialsPath, checkKeyMatchesCredentials, reEncryptKeyMismatchMessage } from '#services/credentials_service.js';
5
+ import { decryptCredentials, credentialsExist, writeEncrypted, resolveCredentialsPath } from '#services/credentials_service.js';
6
6
  const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
7
7
  const detectPathConflict = (obj, dotPath) => {
8
8
  const parts = dotPath.split('.');
@@ -77,11 +77,6 @@ export default class CredentialsSet extends Command {
77
77
  char: 'y',
78
78
  description: 'Skip confirmation prompts when overwriting a value of a different shape',
79
79
  default: false
80
- }),
81
- force: Flags.boolean({
82
- char: 'f',
83
- description: 'Re-encrypt with the current key even if it cannot decrypt the existing file (discards existing values)',
84
- default: false
85
80
  })
86
81
  };
87
82
  async confirmOverwrite(conflict, newPath) {
@@ -105,15 +100,8 @@ export default class CredentialsSet extends Command {
105
100
  if (!credentialsExist(environment, workflow)) {
106
101
  this.error(`No credentials file found at ${resolveCredentialsPath(environment, workflow)}. Run "output credentials init" first.`);
107
102
  }
108
- const keyMismatch = checkKeyMatchesCredentials(environment, workflow) === 'mismatch';
109
- if (keyMismatch && !flags.force) {
110
- this.error(reEncryptKeyMismatchMessage(resolveCredentialsPath(environment, workflow)));
111
- }
112
103
  try {
113
- if (keyMismatch) {
114
- this.warn(reEncryptKeyMismatchMessage(resolveCredentialsPath(environment, workflow)));
115
- }
116
- const plaintext = keyMismatch ? '' : decryptCredentials(environment, workflow);
104
+ const plaintext = decryptCredentials(environment, workflow);
117
105
  const data = (parseYaml(plaintext) || {});
118
106
  const conflict = detectPathConflict(data, args.path);
119
107
  if (conflict && !flags.yes) {
@@ -26,8 +26,6 @@ describe('credentials set command', () => {
26
26
  beforeEach(() => {
27
27
  vi.clearAllMocks();
28
28
  vi.mocked(credentialsService.credentialsExist).mockReturnValue(true);
29
- vi.mocked(credentialsService.checkKeyMatchesCredentials).mockReturnValue('match');
30
- vi.mocked(credentialsService.reEncryptKeyMismatchMessage).mockReturnValue('KEY_MISMATCH_WARNING');
31
29
  vi.mocked(credentialsService.decryptCredentials).mockReturnValue('anthropic:\n api_key: sk-existing\n');
32
30
  vi.mocked(credentialsService.writeEncrypted).mockImplementation(() => { });
33
31
  vi.mocked(confirm).mockResolvedValue(true);
@@ -45,7 +43,7 @@ describe('credentials set command', () => {
45
43
  Object.defineProperty(cmd, 'parse', {
46
44
  value: vi.fn().mockResolvedValue({
47
45
  args: { path: 'anthropic.api_key', value: 'sk-new-key', ...parsedArgs },
48
- flags: { environment: undefined, workflow: undefined, yes: false, force: false, ...flags }
46
+ flags: { environment: undefined, workflow: undefined, yes: false, ...flags }
49
47
  }),
50
48
  configurable: true
51
49
  });
@@ -61,11 +59,10 @@ describe('credentials set command', () => {
61
59
  expect(CredentialsSet.args.value).toBeDefined();
62
60
  expect(CredentialsSet.args.value.required).toBe(true);
63
61
  });
64
- it('should have environment, workflow, yes, and force flags', () => {
62
+ it('should have environment, workflow, and yes flags', () => {
65
63
  expect(CredentialsSet.flags.environment).toBeDefined();
66
64
  expect(CredentialsSet.flags.workflow).toBeDefined();
67
65
  expect(CredentialsSet.flags.yes).toBeDefined();
68
- expect(CredentialsSet.flags.force).toBeDefined();
69
66
  });
70
67
  });
71
68
  describe('command execution', () => {
@@ -124,24 +121,6 @@ describe('credentials set command', () => {
124
121
  await expect(cmd.run()).rejects.toThrow('Failed to update credentials: Permission denied');
125
122
  });
126
123
  });
127
- describe('key mismatch handling', () => {
128
- it('should error and not write when the key cannot decrypt and --force is absent', async () => {
129
- vi.mocked(credentialsService.checkKeyMatchesCredentials).mockReturnValue('mismatch');
130
- const cmd = createTestCommand();
131
- await expect(cmd.run()).rejects.toThrow('KEY_MISMATCH_WARNING');
132
- expect(credentialsService.decryptCredentials).not.toHaveBeenCalled();
133
- expect(credentialsService.writeEncrypted).not.toHaveBeenCalled();
134
- });
135
- it('should warn, skip decryption, and re-encrypt from empty when --force is passed', async () => {
136
- vi.mocked(credentialsService.checkKeyMatchesCredentials).mockReturnValue('mismatch');
137
- const cmd = createTestCommand({}, { force: true });
138
- await cmd.run();
139
- expect(cmd.warn).toHaveBeenCalledWith('KEY_MISMATCH_WARNING');
140
- expect(credentialsService.decryptCredentials).not.toHaveBeenCalled();
141
- expect(credentialsService.writeEncrypted).toHaveBeenCalledTimes(1);
142
- expect(cmd.log).toHaveBeenCalledWith('Set anthropic.api_key');
143
- });
144
- });
145
124
  describe('shape-change confirmation', () => {
146
125
  it('should prompt before converting a primitive value into an object', async () => {
147
126
  vi.mocked(credentialsService.decryptCredentials).mockReturnValue('__PRIMITIVE_AT_X_Y__');
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.6.1-next.83742db.0"
2
+ "framework": "0.6.1-next.f67f4b9.0"
3
3
  }
@@ -4,9 +4,6 @@ export declare const resolveCredentialsPath: (environment: CredentialsEnvironmen
4
4
  export declare const resolveKeyPath: (environment: CredentialsEnvironment, workflow?: WorkflowTarget) => string;
5
5
  export declare const resolveKey: (environment: CredentialsEnvironment, workflow?: WorkflowTarget) => string;
6
6
  export declare const credentialsExist: (environment: CredentialsEnvironment, workflow?: WorkflowTarget) => boolean;
7
- export type KeyMatch = 'no_file' | 'match' | 'mismatch';
8
- export declare const checkKeyMatchesCredentials: (environment: CredentialsEnvironment, workflow?: WorkflowTarget) => KeyMatch;
9
- export declare const reEncryptKeyMismatchMessage: (credPath: string) => string;
10
7
  export declare const decryptCredentials: (environment: CredentialsEnvironment, workflow?: WorkflowTarget) => string;
11
8
  export declare const writeEncrypted: (environment: CredentialsEnvironment, plaintext: string, workflow?: WorkflowTarget) => void;
12
9
  export declare const initCredentials: (environment: CredentialsEnvironment, workflow?: WorkflowTarget) => {
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { load as parseYaml } from 'js-yaml';
6
6
  import { encrypt, decrypt, generateKey } from '@outputai/credentials';
7
- import { initCredentials, decryptCredentials, writeEncrypted, checkKeyMatchesCredentials } from './credentials_service.js';
7
+ import { initCredentials, decryptCredentials, writeEncrypted } from './credentials_service.js';
8
8
  describe('credentials service integration', () => {
9
9
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'output-creds-'));
10
10
  afterAll(() => {
@@ -123,38 +123,4 @@ describe('credentials service integration', () => {
123
123
  expect(fs.readFileSync(prod.credPath).equals(prodCredBytes)).toBe(true);
124
124
  }));
125
125
  });
126
- describe('checkKeyMatchesCredentials', () => {
127
- const withIsolatedProject = (body) => {
128
- const originalCwd = process.cwd();
129
- const originalKey = process.env.OUTPUT_CREDENTIALS_KEY;
130
- delete process.env.OUTPUT_CREDENTIALS_KEY;
131
- const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'output-creds-keymatch-'));
132
- process.chdir(projectDir);
133
- try {
134
- body();
135
- }
136
- finally {
137
- process.chdir(originalCwd);
138
- if (originalKey === undefined) {
139
- delete process.env.OUTPUT_CREDENTIALS_KEY;
140
- }
141
- else {
142
- process.env.OUTPUT_CREDENTIALS_KEY = originalKey;
143
- }
144
- fs.rmSync(projectDir, { recursive: true, force: true });
145
- }
146
- };
147
- it('returns "no_file" when no credentials file exists', () => withIsolatedProject(() => {
148
- expect(checkKeyMatchesCredentials(undefined)).toBe('no_file');
149
- }));
150
- it('returns "match" when the current key decrypts the file', () => withIsolatedProject(() => {
151
- initCredentials(undefined);
152
- expect(checkKeyMatchesCredentials(undefined)).toBe('match');
153
- }));
154
- it('returns "mismatch" when the key cannot decrypt the file', () => withIsolatedProject(() => {
155
- const { keyPath } = initCredentials(undefined);
156
- fs.writeFileSync(keyPath, generateKey(), { mode: 0o600 });
157
- expect(checkKeyMatchesCredentials(undefined)).toBe('mismatch');
158
- }));
159
- });
160
126
  });
@@ -33,25 +33,6 @@ export const resolveKey = (environment, workflow) => {
33
33
  throw new Error(`No key found. Set ${envVar} env var or create ${keyPath}.`);
34
34
  };
35
35
  export const credentialsExist = (environment, workflow) => fs.existsSync(resolveCredentialsPath(environment, workflow));
36
- export const checkKeyMatchesCredentials = (environment, workflow) => {
37
- const credPath = resolveCredentialsPath(environment, workflow);
38
- if (!fs.existsSync(credPath)) {
39
- return 'no_file';
40
- }
41
- try {
42
- const key = resolveKey(environment, workflow);
43
- const ciphertext = fs.readFileSync(credPath, 'utf8').trim();
44
- decrypt(ciphertext, key);
45
- return 'match';
46
- }
47
- catch {
48
- return 'mismatch';
49
- }
50
- };
51
- export const reEncryptKeyMismatchMessage = (credPath) => `The current credentials key cannot decrypt ${credPath}. ` +
52
- 'Continuing will re-encrypt the file with a different key — the wrong key ' +
53
- '(OUTPUT_CREDENTIALS_KEY env var or key file) may be in use, and the existing values will be discarded. ' +
54
- 'Re-run with --force to proceed anyway.';
55
36
  export const decryptCredentials = (environment, workflow) => {
56
37
  const key = resolveKey(environment, workflow);
57
38
  const credPath = resolveCredentialsPath(environment, workflow);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.6.1-next.83742db.0",
3
+ "version": "0.6.1-next.f67f4b9.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.1.0",
38
38
  "yaml": "^2.8.3",
39
- "@outputai/credentials": "0.6.1-next.83742db.0",
40
- "@outputai/evals": "0.6.1-next.83742db.0",
41
- "@outputai/llm": "0.6.1-next.83742db.0"
39
+ "@outputai/credentials": "0.6.1-next.f67f4b9.0",
40
+ "@outputai/evals": "0.6.1-next.f67f4b9.0",
41
+ "@outputai/llm": "0.6.1-next.f67f4b9.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/cli-progress": "3.11.6",