@amodalai/amodal 0.3.90 → 0.3.92
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/CHANGELOG.md +25 -0
- package/dist/src/commands/audit.d.ts +1 -3
- package/dist/src/commands/audit.d.ts.map +1 -1
- package/dist/src/commands/audit.js +4 -53
- package/dist/src/commands/audit.js.map +1 -1
- package/dist/src/commands/build-manifest-types.js +1 -1
- package/dist/src/commands/build-tools.d.ts +3 -10
- package/dist/src/commands/build-tools.d.ts.map +1 -1
- package/dist/src/commands/build-tools.js +5 -118
- package/dist/src/commands/build-tools.js.map +1 -1
- package/dist/src/commands/build.js +1 -1
- package/dist/src/commands/build.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +1 -1
- package/dist/src/commands/deploy.d.ts.map +1 -1
- package/dist/src/commands/deploy.js +3 -61
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/deployments.d.ts.map +1 -1
- package/dist/src/commands/deployments.js +3 -36
- package/dist/src/commands/deployments.js.map +1 -1
- package/dist/src/commands/dev.d.ts.map +1 -1
- package/dist/src/commands/dev.js +7 -10
- package/dist/src/commands/dev.js.map +1 -1
- package/dist/src/commands/experiment.d.ts +1 -3
- package/dist/src/commands/experiment.d.ts.map +1 -1
- package/dist/src/commands/experiment.js +4 -102
- package/dist/src/commands/experiment.js.map +1 -1
- package/dist/src/commands/promote.d.ts.map +1 -1
- package/dist/src/commands/promote.js +3 -21
- package/dist/src/commands/promote.js.map +1 -1
- package/dist/src/commands/rollback.d.ts.map +1 -1
- package/dist/src/commands/rollback.js +3 -24
- package/dist/src/commands/rollback.js.map +1 -1
- package/dist/src/commands/secrets.d.ts.map +1 -1
- package/dist/src/commands/secrets.js +2 -102
- package/dist/src/commands/secrets.js.map +1 -1
- package/dist/src/commands/status.d.ts.map +1 -1
- package/dist/src/commands/status.js +3 -49
- package/dist/src/commands/status.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -8
- package/src/commands/audit.ts +4 -71
- package/src/commands/build-manifest-types.ts +1 -1
- package/src/commands/build-tools.ts +5 -142
- package/src/commands/build.ts +1 -1
- package/src/commands/deploy.test.ts +2 -13
- package/src/commands/deploy.ts +5 -67
- package/src/commands/deployments.ts +3 -39
- package/src/commands/dev.ts +7 -10
- package/src/commands/experiment.ts +4 -110
- package/src/commands/promote.ts +3 -21
- package/src/commands/rollback.ts +3 -24
- package/src/commands/secrets.test.ts +12 -134
- package/src/commands/secrets.ts +2 -116
- package/src/commands/status.ts +3 -51
- package/dist/src/shared/platform-client.d.ts +0 -110
- package/dist/src/shared/platform-client.d.ts.map +0 -1
- package/dist/src/shared/platform-client.js +0 -263
- package/dist/src/shared/platform-client.js.map +0 -1
- package/src/commands/audit.test.ts +0 -92
- package/src/commands/experiment.test.ts +0 -125
- package/src/shared/platform-client.test.ts +0 -70
- package/src/shared/platform-client.ts +0 -343
|
@@ -5,128 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {CommandModule} from 'yargs';
|
|
8
|
-
import {resolvePlatformConfig} from '../shared/platform-client.js';
|
|
9
8
|
|
|
10
9
|
export interface ExperimentOptions {
|
|
11
10
|
action: 'create' | 'deploy' | 'watch' | 'list';
|
|
12
11
|
name?: string;
|
|
13
12
|
id?: string;
|
|
14
|
-
platformUrl?: string;
|
|
15
|
-
platformApiKey?: string;
|
|
16
13
|
controlConfig?: string;
|
|
17
14
|
variantConfig?: string;
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
18
|
+
* Hosted experiment management is not available from the OSS CLI.
|
|
22
19
|
*/
|
|
23
20
|
export async function runExperimentCommand(options: ExperimentOptions): Promise<void> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const config = await resolvePlatformConfig({
|
|
28
|
-
url: options.platformUrl,
|
|
29
|
-
apiKey: options.platformApiKey,
|
|
30
|
-
});
|
|
31
|
-
platformUrl = config.url;
|
|
32
|
-
apiKey = config.apiKey;
|
|
33
|
-
} catch (err) {
|
|
34
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
-
process.stderr.write(`[experiment] ${msg}\n`);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const headers = {
|
|
40
|
-
'Content-Type': 'application/json',
|
|
41
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
switch (options.action) {
|
|
45
|
-
case 'list': {
|
|
46
|
-
const res = await fetch(`${platformUrl}/api/experiments`, {headers});
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
process.stderr.write(`[experiment] HTTP ${res.status}\n`);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- platform response
|
|
52
|
-
const data = await res.json() as {experiments: Array<Record<string, unknown>>};
|
|
53
|
-
if (data.experiments.length === 0) {
|
|
54
|
-
process.stdout.write('No experiments found.\n');
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
for (const exp of data.experiments) {
|
|
58
|
-
process.stdout.write(`${exp['id']} ${exp['name']} [${exp['status']}]\n`);
|
|
59
|
-
}
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
case 'create': {
|
|
64
|
-
if (!options.name) {
|
|
65
|
-
process.stderr.write('[experiment] --name is required for create\n');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const controlConfig: Record<string, unknown> = options.controlConfig
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- CLI JSON input
|
|
70
|
-
? JSON.parse(options.controlConfig) as Record<string, unknown>
|
|
71
|
-
: {};
|
|
72
|
-
const variantConfig: Record<string, unknown> = options.variantConfig
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- CLI JSON input
|
|
74
|
-
? JSON.parse(options.variantConfig) as Record<string, unknown>
|
|
75
|
-
: {};
|
|
76
|
-
|
|
77
|
-
const res = await fetch(`${platformUrl}/api/experiments`, {
|
|
78
|
-
method: 'POST',
|
|
79
|
-
headers,
|
|
80
|
-
body: JSON.stringify({name: options.name, controlConfig, variantConfig}),
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
process.stderr.write(`[experiment] HTTP ${res.status}\n`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- platform response
|
|
87
|
-
const {id} = await res.json() as {id: string};
|
|
88
|
-
process.stdout.write(`Created experiment: ${id}\n`);
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
case 'deploy': {
|
|
93
|
-
if (!options.id) {
|
|
94
|
-
process.stderr.write('[experiment] --id is required for deploy\n');
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const res = await fetch(`${platformUrl}/api/experiments/${options.id}`, {
|
|
98
|
-
method: 'PUT',
|
|
99
|
-
headers,
|
|
100
|
-
body: JSON.stringify({status: 'deployed'}),
|
|
101
|
-
});
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
process.stderr.write(`[experiment] HTTP ${res.status}\n`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
process.stdout.write(`Deployed experiment: ${options.id}\n`);
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case 'watch': {
|
|
111
|
-
if (!options.id) {
|
|
112
|
-
process.stderr.write('[experiment] --id is required for watch\n');
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const res = await fetch(`${platformUrl}/api/experiments/${options.id}`, {headers});
|
|
116
|
-
if (!res.ok) {
|
|
117
|
-
process.stderr.write(`[experiment] HTTP ${res.status}\n`);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- platform response
|
|
121
|
-
const exp = await res.json() as Record<string, unknown>;
|
|
122
|
-
process.stdout.write(JSON.stringify(exp, null, 2) + '\n');
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
default:
|
|
127
|
-
process.stderr.write(`[experiment] Unknown action: ${options.action}\n`);
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
21
|
+
void options;
|
|
22
|
+
process.stderr.write('[experiment] Hosted experiment management is not included in the OSS CLI.\n');
|
|
23
|
+
process.exit(1);
|
|
130
24
|
}
|
|
131
25
|
|
|
132
26
|
export const experimentCommand: CommandModule = {
|
package/src/commands/promote.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {CommandModule} from 'yargs';
|
|
8
|
-
import {PlatformClient} from '../shared/platform-client.js';
|
|
9
8
|
|
|
10
9
|
export interface PromoteOptions {
|
|
11
10
|
fromEnv: string;
|
|
@@ -16,26 +15,9 @@ export interface PromoteOptions {
|
|
|
16
15
|
* Promote a deployment from one environment to another.
|
|
17
16
|
*/
|
|
18
17
|
export async function runPromote(options: PromoteOptions): Promise<number> {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch (err) {
|
|
23
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24
|
-
process.stderr.write(`[promote] ${msg}\n`);
|
|
25
|
-
return 1;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const toEnv = options.toEnv ?? 'production';
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const result = await client.promote(options.fromEnv, toEnv);
|
|
32
|
-
process.stderr.write(`[promote] Promoted from ${options.fromEnv} to ${toEnv}: ${result.id}\n`);
|
|
33
|
-
return 0;
|
|
34
|
-
} catch (err) {
|
|
35
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
-
process.stderr.write(`[promote] Failed: ${msg}\n`);
|
|
37
|
-
return 1;
|
|
38
|
-
}
|
|
18
|
+
void options;
|
|
19
|
+
process.stderr.write('[promote] Hosted deployment promotion is not included in the OSS CLI.\n');
|
|
20
|
+
return 1;
|
|
39
21
|
}
|
|
40
22
|
|
|
41
23
|
export const promoteCommand: CommandModule = {
|
package/src/commands/rollback.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {CommandModule} from 'yargs';
|
|
8
|
-
import {PlatformClient} from '../shared/platform-client.js';
|
|
9
8
|
|
|
10
9
|
export interface RollbackOptions {
|
|
11
10
|
deployId?: string;
|
|
@@ -17,29 +16,9 @@ export interface RollbackOptions {
|
|
|
17
16
|
* Rollback to a previous deployment.
|
|
18
17
|
*/
|
|
19
18
|
export async function runRollback(options: RollbackOptions = {}): Promise<number> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} catch (err) {
|
|
24
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
25
|
-
process.stderr.write(`[rollback] ${msg}\n`);
|
|
26
|
-
return 1;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const environment = options.env ?? 'production';
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const result = await client.rollback({
|
|
33
|
-
deployId: options.deployId,
|
|
34
|
-
environment,
|
|
35
|
-
});
|
|
36
|
-
process.stderr.write(`[rollback] Rolled back to ${result.id} in ${result.environment}\n`);
|
|
37
|
-
return 0;
|
|
38
|
-
} catch (err) {
|
|
39
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
40
|
-
process.stderr.write(`[rollback] Failed: ${msg}\n`);
|
|
41
|
-
return 1;
|
|
42
|
-
}
|
|
19
|
+
void options;
|
|
20
|
+
process.stderr.write('[rollback] Hosted deployment rollback is not included in the OSS CLI.\n');
|
|
21
|
+
return 1;
|
|
43
22
|
}
|
|
44
23
|
|
|
45
24
|
export const rollbackCommand: CommandModule = {
|
|
@@ -4,22 +4,15 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
8
8
|
|
|
9
9
|
describe('runSecrets', () => {
|
|
10
10
|
let stderrOutput: string;
|
|
11
11
|
let stdoutOutput: string;
|
|
12
|
-
|
|
13
|
-
let fetchSpy: any;
|
|
14
|
-
const originalEnv = process.env;
|
|
12
|
+
let fetchSpy: ReturnType<typeof vi.spyOn>;
|
|
15
13
|
|
|
16
14
|
beforeEach(() => {
|
|
17
15
|
vi.clearAllMocks();
|
|
18
|
-
process.env = {
|
|
19
|
-
...originalEnv,
|
|
20
|
-
PLATFORM_API_URL: 'https://api.example.com',
|
|
21
|
-
PLATFORM_API_KEY: 'test-key',
|
|
22
|
-
};
|
|
23
16
|
stderrOutput = '';
|
|
24
17
|
stdoutOutput = '';
|
|
25
18
|
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
@@ -34,135 +27,20 @@ describe('runSecrets', () => {
|
|
|
34
27
|
});
|
|
35
28
|
|
|
36
29
|
afterEach(() => {
|
|
37
|
-
|
|
38
|
-
fetchSpy.mockRestore();
|
|
30
|
+
vi.restoreAllMocks();
|
|
39
31
|
});
|
|
40
32
|
|
|
41
|
-
it(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(result).toBe(0);
|
|
47
|
-
expect(stderrOutput).toContain('set successfully');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should return 1 when key missing for set', async () => {
|
|
51
|
-
const {runSecrets} = await import('./secrets.js');
|
|
52
|
-
const result = await runSecrets({subcommand: 'set'});
|
|
53
|
-
expect(result).toBe(1);
|
|
54
|
-
expect(stderrOutput).toContain('Missing key');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should return 1 when value missing for set', async () => {
|
|
58
|
-
const {runSecrets} = await import('./secrets.js');
|
|
59
|
-
const result = await runSecrets({subcommand: 'set', key: 'DB_URL'});
|
|
60
|
-
expect(result).toBe(1);
|
|
61
|
-
expect(stderrOutput).toContain('Missing value');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should list secrets', async () => {
|
|
65
|
-
fetchSpy.mockResolvedValue(new Response(JSON.stringify([
|
|
66
|
-
{key: 'DB_URL'},
|
|
67
|
-
{key: 'API_KEY'},
|
|
68
|
-
]), {status: 200}));
|
|
69
|
-
|
|
70
|
-
const {runSecrets} = await import('./secrets.js');
|
|
71
|
-
const result = await runSecrets({subcommand: 'list'});
|
|
72
|
-
expect(result).toBe(0);
|
|
73
|
-
expect(stdoutOutput).toContain('DB_URL');
|
|
74
|
-
expect(stdoutOutput).toContain('API_KEY');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should list secrets as JSON', async () => {
|
|
78
|
-
fetchSpy.mockResolvedValue(new Response(JSON.stringify([{key: 'DB_URL'}]), {status: 200}));
|
|
79
|
-
|
|
80
|
-
const {runSecrets} = await import('./secrets.js');
|
|
81
|
-
const result = await runSecrets({subcommand: 'list', json: true});
|
|
82
|
-
expect(result).toBe(0);
|
|
83
|
-
const parsed = JSON.parse(stdoutOutput);
|
|
84
|
-
expect(parsed).toHaveLength(1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle empty secrets list', async () => {
|
|
88
|
-
fetchSpy.mockResolvedValue(new Response('[]', {status: 200}));
|
|
89
|
-
|
|
33
|
+
it.each([
|
|
34
|
+
{subcommand: 'set' as const, key: 'DB_URL', value: 'postgres://example'},
|
|
35
|
+
{subcommand: 'list' as const},
|
|
36
|
+
{subcommand: 'delete' as const, key: 'DB_URL'},
|
|
37
|
+
])('fails closed for hosted secret management in OSS: $subcommand', async (options) => {
|
|
90
38
|
const {runSecrets} = await import('./secrets.js');
|
|
91
|
-
const result = await runSecrets(
|
|
92
|
-
expect(result).toBe(0);
|
|
93
|
-
expect(stderrOutput).toContain('No secrets configured');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should delete a secret', async () => {
|
|
97
|
-
fetchSpy.mockResolvedValue(new Response('{}', {status: 200}));
|
|
39
|
+
const result = await runSecrets(options);
|
|
98
40
|
|
|
99
|
-
const {runSecrets} = await import('./secrets.js');
|
|
100
|
-
const result = await runSecrets({subcommand: 'delete', key: 'DB_URL'});
|
|
101
|
-
expect(result).toBe(0);
|
|
102
|
-
expect(stderrOutput).toContain('deleted');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should return 1 when key missing for delete', async () => {
|
|
106
|
-
const {runSecrets} = await import('./secrets.js');
|
|
107
|
-
const result = await runSecrets({subcommand: 'delete'});
|
|
108
41
|
expect(result).toBe(1);
|
|
109
|
-
expect(stderrOutput).toContain('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
it('should return 1 when platform not configured', async () => {
|
|
113
|
-
delete process.env['PLATFORM_API_URL'];
|
|
114
|
-
delete process.env['PLATFORM_API_KEY'];
|
|
115
|
-
|
|
116
|
-
const {runSecrets} = await import('./secrets.js');
|
|
117
|
-
const result = await runSecrets({subcommand: 'list'});
|
|
118
|
-
expect(result).toBe(1);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should handle auth failure', async () => {
|
|
122
|
-
fetchSpy.mockResolvedValue(new Response('Unauthorized', {status: 401}));
|
|
123
|
-
|
|
124
|
-
const {runSecrets} = await import('./secrets.js');
|
|
125
|
-
const result = await runSecrets({subcommand: 'list'});
|
|
126
|
-
expect(result).toBe(1);
|
|
127
|
-
expect(stderrOutput).toContain('401');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should handle network error', async () => {
|
|
131
|
-
fetchSpy.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
132
|
-
|
|
133
|
-
const {runSecrets} = await import('./secrets.js');
|
|
134
|
-
const result = await runSecrets({subcommand: 'set', key: 'K', value: 'V'});
|
|
135
|
-
expect(result).toBe(1);
|
|
136
|
-
expect(stderrOutput).toContain('ECONNREFUSED');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should handle set server error', async () => {
|
|
140
|
-
fetchSpy.mockResolvedValue(new Response('Internal Error', {status: 500}));
|
|
141
|
-
|
|
142
|
-
const {runSecrets} = await import('./secrets.js');
|
|
143
|
-
const result = await runSecrets({subcommand: 'set', key: 'K', value: 'V'});
|
|
144
|
-
expect(result).toBe(1);
|
|
145
|
-
expect(stderrOutput).toContain('500');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should show count for listed secrets', async () => {
|
|
149
|
-
fetchSpy.mockResolvedValue(new Response(JSON.stringify([
|
|
150
|
-
{key: 'A'},
|
|
151
|
-
{key: 'B'},
|
|
152
|
-
{key: 'C'},
|
|
153
|
-
]), {status: 200}));
|
|
154
|
-
|
|
155
|
-
const {runSecrets} = await import('./secrets.js');
|
|
156
|
-
await runSecrets({subcommand: 'list'});
|
|
157
|
-
expect(stderrOutput).toContain('3 secrets');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should use singular for single secret', async () => {
|
|
161
|
-
fetchSpy.mockResolvedValue(new Response(JSON.stringify([{key: 'ONLY'}]), {status: 200}));
|
|
162
|
-
|
|
163
|
-
const {runSecrets} = await import('./secrets.js');
|
|
164
|
-
await runSecrets({subcommand: 'list'});
|
|
165
|
-
expect(stderrOutput).toContain('1 secret configured');
|
|
166
|
-
expect(stderrOutput).not.toContain('1 secrets');
|
|
42
|
+
expect(stderrOutput).toContain('Hosted secret management is not included in the OSS CLI');
|
|
43
|
+
expect(stdoutOutput).toBe('');
|
|
44
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
167
45
|
});
|
|
168
46
|
});
|
package/src/commands/secrets.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {CommandModule} from 'yargs';
|
|
8
|
-
import {resolvePlatformConfig} from '../shared/platform-client.js';
|
|
9
8
|
|
|
10
9
|
export interface SecretsOptions {
|
|
11
10
|
cwd?: string;
|
|
@@ -20,121 +19,8 @@ export interface SecretsOptions {
|
|
|
20
19
|
* Returns 0 on success, 1 on error.
|
|
21
20
|
*/
|
|
22
21
|
export async function runSecrets(options: SecretsOptions): Promise<number> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
config = await resolvePlatformConfig();
|
|
26
|
-
} catch (err) {
|
|
27
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
28
|
-
process.stderr.write(`[secrets] ${msg}\n`);
|
|
29
|
-
return 1;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const headers: Record<string, string> = {
|
|
33
|
-
'Authorization': `Bearer ${config.apiKey}`,
|
|
34
|
-
'Content-Type': 'application/json',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
switch (options.subcommand) {
|
|
38
|
-
case 'set': {
|
|
39
|
-
if (!options.key) {
|
|
40
|
-
process.stderr.write('[secrets] Missing key. Usage: amodal secrets set <key> <value>\n');
|
|
41
|
-
return 1;
|
|
42
|
-
}
|
|
43
|
-
if (options.value === undefined) {
|
|
44
|
-
process.stderr.write('[secrets] Missing value. Usage: amodal secrets set <key> <value>\n');
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const response = await fetch(`${config.url}/api/secrets`, {
|
|
50
|
-
method: 'PUT',
|
|
51
|
-
headers,
|
|
52
|
-
body: JSON.stringify({key: options.key, value: options.value}),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
process.stderr.write(`[secrets] Failed to set secret: ${response.status} ${response.statusText}\n`);
|
|
57
|
-
return 1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
process.stderr.write(`[secrets] Secret "${options.key}" set successfully.\n`);
|
|
61
|
-
return 0;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
64
|
-
process.stderr.write(`[secrets] Failed to set secret: ${msg}\n`);
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
case 'list': {
|
|
70
|
-
try {
|
|
71
|
-
const response = await fetch(`${config.url}/api/secrets`, {
|
|
72
|
-
method: 'GET',
|
|
73
|
-
headers,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
process.stderr.write(`[secrets] Failed to list secrets: ${response.status} ${response.statusText}\n`);
|
|
78
|
-
return 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
82
|
-
const secrets = (await response.json()) as Array<{key: string}>;
|
|
83
|
-
|
|
84
|
-
if (secrets.length === 0) {
|
|
85
|
-
process.stderr.write('[secrets] No secrets configured.\n');
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (options.json) {
|
|
90
|
-
process.stdout.write(JSON.stringify(secrets, null, 2) + '\n');
|
|
91
|
-
return 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
process.stdout.write('KEY\n');
|
|
95
|
-
for (const secret of secrets) {
|
|
96
|
-
process.stdout.write(`${secret.key}\n`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
process.stderr.write(`[secrets] ${secrets.length} secret${secrets.length === 1 ? '' : 's'} configured.\n`);
|
|
100
|
-
return 0;
|
|
101
|
-
} catch (err) {
|
|
102
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
103
|
-
process.stderr.write(`[secrets] Failed to list secrets: ${msg}\n`);
|
|
104
|
-
return 1;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
case 'delete': {
|
|
109
|
-
if (!options.key) {
|
|
110
|
-
process.stderr.write('[secrets] Missing key. Usage: amodal secrets delete <key>\n');
|
|
111
|
-
return 1;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const response = await fetch(`${config.url}/api/secrets/${encodeURIComponent(options.key)}`, {
|
|
116
|
-
method: 'DELETE',
|
|
117
|
-
headers,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
process.stderr.write(`[secrets] Failed to delete secret: ${response.status} ${response.statusText}\n`);
|
|
122
|
-
return 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
process.stderr.write(`[secrets] Secret "${options.key}" deleted.\n`);
|
|
126
|
-
return 0;
|
|
127
|
-
} catch (err) {
|
|
128
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
-
process.stderr.write(`[secrets] Failed to delete secret: ${msg}\n`);
|
|
130
|
-
return 1;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
default:
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
|
|
22
|
+
void options;
|
|
23
|
+
process.stderr.write('[secrets] Hosted secret management is not included in the OSS CLI.\n');
|
|
138
24
|
return 1;
|
|
139
25
|
}
|
|
140
26
|
|
package/src/commands/status.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {CommandModule} from 'yargs';
|
|
8
|
-
import {PlatformClient} from '../shared/platform-client.js';
|
|
9
8
|
|
|
10
9
|
export interface StatusOptions {
|
|
11
10
|
env?: string;
|
|
@@ -16,56 +15,9 @@ export interface StatusOptions {
|
|
|
16
15
|
* Show current deployment status per environment.
|
|
17
16
|
*/
|
|
18
17
|
export async function runStatus(options: StatusOptions = {}): Promise<number> {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch (err) {
|
|
23
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
24
|
-
process.stderr.write(`[status] ${msg}\n`);
|
|
25
|
-
return 1;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const environments = options.env ? [options.env] : ['production', 'staging'];
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const results: Array<{environment: string; id?: string; createdAt?: string; createdBy?: string; commitSha?: string}> = [];
|
|
32
|
-
|
|
33
|
-
for (const env of environments) {
|
|
34
|
-
const deployments = await client.listDeployments({environment: env, limit: 1});
|
|
35
|
-
const active = deployments.find((d) => d.isActive);
|
|
36
|
-
if (active) {
|
|
37
|
-
results.push({
|
|
38
|
-
environment: env,
|
|
39
|
-
id: active.id,
|
|
40
|
-
createdAt: active.createdAt,
|
|
41
|
-
createdBy: active.createdBy ?? undefined,
|
|
42
|
-
commitSha: active.commitSha ?? undefined,
|
|
43
|
-
});
|
|
44
|
-
} else {
|
|
45
|
-
results.push({environment: env});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (options.json) {
|
|
50
|
-
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for (const r of results) {
|
|
55
|
-
if (r.id) {
|
|
56
|
-
const sha = r.commitSha ? ` (${r.commitSha.slice(0, 7)})` : '';
|
|
57
|
-
process.stdout.write(`${r.environment}: ${r.id}${sha} — ${r.createdBy ?? 'unknown'} at ${r.createdAt}\n`);
|
|
58
|
-
} else {
|
|
59
|
-
process.stdout.write(`${r.environment}: no active deployment\n`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return 0;
|
|
64
|
-
} catch (err) {
|
|
65
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
66
|
-
process.stderr.write(`[status] Failed: ${msg}\n`);
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
18
|
+
void options;
|
|
19
|
+
process.stderr.write('[status] Hosted deployment status is not included in the OSS CLI.\n');
|
|
20
|
+
return 1;
|
|
69
21
|
}
|
|
70
22
|
|
|
71
23
|
export const statusCommand: CommandModule = {
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Amodal Labs, Inc.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Metadata returned for a deployment by the platform API.
|
|
8
|
-
*/
|
|
9
|
-
export interface DeploymentMeta {
|
|
10
|
-
id: string;
|
|
11
|
-
environment: string;
|
|
12
|
-
isActive: boolean;
|
|
13
|
-
createdAt: string;
|
|
14
|
-
createdBy: string;
|
|
15
|
-
source: string;
|
|
16
|
-
commitSha?: string;
|
|
17
|
-
branch?: string;
|
|
18
|
-
message?: string;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Resolve platform URL and API key from multiple sources:
|
|
22
|
-
* 1. Explicit options (flags)
|
|
23
|
-
* 2. .amodal/project.json (platformUrl from `amodal link`)
|
|
24
|
-
* 3. ~/.amodalrc (auth token from `amodal login`)
|
|
25
|
-
* 4. Env vars (fallback)
|
|
26
|
-
*/
|
|
27
|
-
export declare function resolvePlatformConfig(options?: {
|
|
28
|
-
url?: string;
|
|
29
|
-
apiKey?: string;
|
|
30
|
-
}): Promise<{
|
|
31
|
-
url: string;
|
|
32
|
-
apiKey: string;
|
|
33
|
-
}>;
|
|
34
|
-
/**
|
|
35
|
-
* Platform API client for snapshot deployments.
|
|
36
|
-
*
|
|
37
|
-
* Resolves credentials from: explicit options → project link → rc file → env vars.
|
|
38
|
-
* Use `PlatformClient.create()` for async auto-discovery, or `new PlatformClient()` for sync usage.
|
|
39
|
-
*/
|
|
40
|
-
export declare class PlatformClient {
|
|
41
|
-
private readonly baseUrl;
|
|
42
|
-
private readonly apiKey;
|
|
43
|
-
constructor(options?: {
|
|
44
|
-
url?: string;
|
|
45
|
-
apiKey?: string;
|
|
46
|
-
});
|
|
47
|
-
/**
|
|
48
|
-
* Create a PlatformClient with auto-discovery of credentials.
|
|
49
|
-
* Resolves from: explicit options → project link → rc file → env vars.
|
|
50
|
-
*/
|
|
51
|
-
static create(options?: {
|
|
52
|
-
url?: string;
|
|
53
|
-
apiKey?: string;
|
|
54
|
-
}): Promise<PlatformClient>;
|
|
55
|
-
private headers;
|
|
56
|
-
private request;
|
|
57
|
-
/**
|
|
58
|
-
* Try to refresh the token using the stored refresh token.
|
|
59
|
-
* Updates both the in-memory key and the rc file on success.
|
|
60
|
-
*/
|
|
61
|
-
private tryRefreshToken;
|
|
62
|
-
/**
|
|
63
|
-
* Trigger a runtime-app build on the build server.
|
|
64
|
-
* Sends the repo tarball to the build server which builds the SPA and uploads to R2.
|
|
65
|
-
*/
|
|
66
|
-
triggerBuild(buildServerUrl: string, appId: string, deployId: string, repoTarball: import('node:fs').ReadStream): Promise<void>;
|
|
67
|
-
/**
|
|
68
|
-
* Trigger a remote build:
|
|
69
|
-
* 1. Get scoped R2 temp credentials from the platform API
|
|
70
|
-
* 2. Upload the tarball directly to R2 with those creds
|
|
71
|
-
* 3. Tell the platform API to trigger a Fly Machine build
|
|
72
|
-
*
|
|
73
|
-
* Returns a buildId for polling.
|
|
74
|
-
*/
|
|
75
|
-
triggerRemoteBuild(appId: string, environment: string, tarballPath: string, message?: string): Promise<{
|
|
76
|
-
buildId: string;
|
|
77
|
-
}>;
|
|
78
|
-
/**
|
|
79
|
-
* Poll build status.
|
|
80
|
-
*/
|
|
81
|
-
getBuildStatus(buildId: string): Promise<{
|
|
82
|
-
status: 'building' | 'complete' | 'error';
|
|
83
|
-
deployId?: string;
|
|
84
|
-
environment?: string;
|
|
85
|
-
error?: string;
|
|
86
|
-
}>;
|
|
87
|
-
/**
|
|
88
|
-
* List deployments for the authenticated app.
|
|
89
|
-
*/
|
|
90
|
-
listDeployments(options?: {
|
|
91
|
-
environment?: string;
|
|
92
|
-
limit?: number;
|
|
93
|
-
}): Promise<DeploymentMeta[]>;
|
|
94
|
-
/**
|
|
95
|
-
* Rollback to a previous deployment.
|
|
96
|
-
*/
|
|
97
|
-
rollback(options?: {
|
|
98
|
-
deployId?: string;
|
|
99
|
-
environment?: string;
|
|
100
|
-
}): Promise<DeploymentMeta>;
|
|
101
|
-
/**
|
|
102
|
-
* Promote a deployment from one environment to another.
|
|
103
|
-
*/
|
|
104
|
-
promote(fromEnv: string, toEnv?: string): Promise<DeploymentMeta>;
|
|
105
|
-
/**
|
|
106
|
-
* Get status of a specific deployment.
|
|
107
|
-
*/
|
|
108
|
-
getStatus(deployId: string): Promise<DeploymentMeta>;
|
|
109
|
-
}
|
|
110
|
-
//# sourceMappingURL=platform-client.d.ts.map
|