@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.
- package/dist/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/credentials/edit.d.ts +0 -1
- package/dist/commands/credentials/edit.js +2 -14
- package/dist/commands/credentials/edit.spec.js +2 -24
- package/dist/commands/credentials/set.d.ts +0 -1
- package/dist/commands/credentials/set.js +2 -14
- package/dist/commands/credentials/set.spec.js +2 -23
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/credentials_service.d.ts +0 -3
- package/dist/services/credentials_service.integration.test.js +1 -35
- package/dist/services/credentials_service.js +0 -19
- package/package.json +4 -4
|
@@ -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
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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__');
|
|
@@ -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
|
|
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.
|
|
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.
|
|
40
|
-
"@outputai/evals": "0.6.1-next.
|
|
41
|
-
"@outputai/llm": "0.6.1-next.
|
|
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",
|