@amodalai/amodal 0.1.6 → 0.1.9
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 +45 -0
- package/dist/src/commands/chat.d.ts.map +1 -1
- package/dist/src/commands/chat.js +38 -2
- package/dist/src/commands/chat.js.map +1 -1
- package/dist/src/commands/deploy.d.ts.map +1 -1
- package/dist/src/commands/deploy.js +16 -1
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/dev.d.ts +1 -0
- package/dist/src/commands/dev.d.ts.map +1 -1
- package/dist/src/commands/dev.js +33 -33
- package/dist/src/commands/dev.js.map +1 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/login.d.ts +5 -0
- package/dist/src/commands/login.d.ts.map +1 -1
- package/dist/src/commands/login.js +70 -4
- package/dist/src/commands/login.js.map +1 -1
- package/dist/src/commands/validate.d.ts +1 -0
- package/dist/src/commands/validate.d.ts.map +1 -1
- package/dist/src/commands/validate.js +228 -4
- package/dist/src/commands/validate.js.map +1 -1
- package/dist/src/shared/connection-preflight.d.ts +52 -0
- package/dist/src/shared/connection-preflight.d.ts.map +1 -0
- package/dist/src/shared/connection-preflight.js +204 -0
- package/dist/src/shared/connection-preflight.js.map +1 -0
- package/dist/src/shared/platform-client.d.ts +14 -1
- package/dist/src/shared/platform-client.d.ts.map +1 -1
- package/dist/src/shared/platform-client.js +103 -2
- package/dist/src/shared/platform-client.js.map +1 -1
- package/dist/src/shared/tarball.d.ts +13 -0
- package/dist/src/shared/tarball.d.ts.map +1 -0
- package/dist/src/shared/tarball.js +21 -0
- package/dist/src/shared/tarball.js.map +1 -0
- package/dist/src/ui/ChatApp.d.ts.map +1 -1
- package/dist/src/ui/ChatApp.js +8 -7
- package/dist/src/ui/ChatApp.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/commands/chat.ts +38 -2
- package/src/commands/deploy.ts +17 -1
- package/src/commands/dev.ts +35 -32
- package/src/commands/index.ts +2 -1
- package/src/commands/login.ts +76 -4
- package/src/commands/validate.test.ts +169 -16
- package/src/commands/validate.ts +272 -4
- package/src/shared/connection-preflight.ts +251 -0
- package/src/shared/platform-client.ts +115 -2
- package/src/shared/tarball.ts +30 -0
- package/src/ui/ChatApp.tsx +8 -7
|
@@ -105,12 +105,24 @@ export class PlatformClient {
|
|
|
105
105
|
|
|
106
106
|
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
107
107
|
const url = `${this.baseUrl}${path}`;
|
|
108
|
-
|
|
108
|
+
let resp = await fetch(url, {
|
|
109
109
|
method,
|
|
110
110
|
headers: this.headers(),
|
|
111
111
|
...(body ? {body: JSON.stringify(body)} : {}),
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// Auto-refresh on 401
|
|
115
|
+
if (resp.status === 401) {
|
|
116
|
+
const refreshed = await this.tryRefreshToken();
|
|
117
|
+
if (refreshed) {
|
|
118
|
+
resp = await fetch(url, {
|
|
119
|
+
method,
|
|
120
|
+
headers: this.headers(),
|
|
121
|
+
...(body ? {body: JSON.stringify(body)} : {}),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
if (!resp.ok) {
|
|
115
127
|
let detail = '';
|
|
116
128
|
try {
|
|
@@ -128,7 +140,48 @@ export class PlatformClient {
|
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
/**
|
|
131
|
-
*
|
|
143
|
+
* Try to refresh the token using the stored refresh token.
|
|
144
|
+
* Updates both the in-memory key and the rc file on success.
|
|
145
|
+
*/
|
|
146
|
+
private async tryRefreshToken(): Promise<boolean> {
|
|
147
|
+
try {
|
|
148
|
+
const {readRcFile} = await import('../commands/login.js');
|
|
149
|
+
const rc = await readRcFile();
|
|
150
|
+
if (!rc.platform?.refreshToken) return false;
|
|
151
|
+
|
|
152
|
+
const res = await fetch(`${this.baseUrl}/api/auth/refresh`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: {'Content-Type': 'application/json'},
|
|
155
|
+
body: JSON.stringify({refresh_token: rc.platform.refreshToken}),
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) return false;
|
|
158
|
+
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
160
|
+
const data = (await res.json()) as {access_token?: string; refresh_token?: string};
|
|
161
|
+
if (!data.access_token) return false;
|
|
162
|
+
|
|
163
|
+
// Update in-memory
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- private field update
|
|
165
|
+
(this as unknown as {apiKey: string}).apiKey = data.access_token;
|
|
166
|
+
|
|
167
|
+
// Persist to rc file
|
|
168
|
+
rc.platform.token = data.access_token;
|
|
169
|
+
if (data.refresh_token) rc.platform.refreshToken = data.refresh_token;
|
|
170
|
+
const {writeFile} = await import('node:fs/promises');
|
|
171
|
+
const {homedir} = await import('node:os');
|
|
172
|
+
const path = await import('node:path');
|
|
173
|
+
const rcPath = path.join(homedir(), '.amodalrc');
|
|
174
|
+
await writeFile(rcPath, JSON.stringify(rc, null, 2) + '\n', {mode: 0o600});
|
|
175
|
+
|
|
176
|
+
process.stderr.write('[auth] Token refreshed automatically.\n');
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Upload a snapshot and deploy it (without repo files).
|
|
132
185
|
*/
|
|
133
186
|
async uploadSnapshot(snapshot: DeploySnapshot, options: {
|
|
134
187
|
environment?: string;
|
|
@@ -139,6 +192,66 @@ export class PlatformClient {
|
|
|
139
192
|
});
|
|
140
193
|
}
|
|
141
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Deploy with repo files — uploads snapshot + repo tarball.
|
|
197
|
+
* The server builds the runtime-app and uploads to R2.
|
|
198
|
+
*/
|
|
199
|
+
async deployWithRepo(
|
|
200
|
+
snapshot: DeploySnapshot,
|
|
201
|
+
repoTarball: import('node:fs').ReadStream,
|
|
202
|
+
options: { environment?: string; appId?: string } = {},
|
|
203
|
+
): Promise<SnapshotDeploymentMeta> {
|
|
204
|
+
const url = `${this.baseUrl}/api/snapshot-deployments`;
|
|
205
|
+
|
|
206
|
+
const formData = new FormData();
|
|
207
|
+
formData.append('snapshot', JSON.stringify(snapshot));
|
|
208
|
+
formData.append('environment', options.environment ?? 'production');
|
|
209
|
+
if (options.appId) {
|
|
210
|
+
formData.append('appId', options.appId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Convert ReadStream to Blob for FormData
|
|
214
|
+
const chunks: Uint8Array[] = [];
|
|
215
|
+
for await (const chunk of repoTarball) {
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ReadStream chunks are Buffer/Uint8Array
|
|
217
|
+
chunks.push(chunk as Uint8Array);
|
|
218
|
+
}
|
|
219
|
+
const blob = new Blob(chunks, {type: 'application/gzip'});
|
|
220
|
+
formData.append('repo', blob, 'repo.tar.gz');
|
|
221
|
+
|
|
222
|
+
let resp = await fetch(url, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {Authorization: `Bearer ${this.apiKey}`},
|
|
225
|
+
body: formData,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Auto-refresh on 401
|
|
229
|
+
if (resp.status === 401) {
|
|
230
|
+
const refreshed = await this.tryRefreshToken();
|
|
231
|
+
if (refreshed) {
|
|
232
|
+
// Re-read the tarball - can't reuse the stream
|
|
233
|
+
resp = await fetch(url, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: {Authorization: `Bearer ${this.apiKey}`},
|
|
236
|
+
body: formData,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!resp.ok) {
|
|
242
|
+
let detail = '';
|
|
243
|
+
try {
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
245
|
+
const errBody = await resp.json() as {error?: string};
|
|
246
|
+
detail = errBody.error ? `: ${errBody.error}` : '';
|
|
247
|
+
} catch { /* ignore */ }
|
|
248
|
+
throw new Error(`Platform API POST /api/snapshot-deployments failed (${resp.status})${detail}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
252
|
+
return resp.json() as Promise<SnapshotDeploymentMeta>;
|
|
253
|
+
}
|
|
254
|
+
|
|
142
255
|
/**
|
|
143
256
|
* List deployments for the authenticated tenant.
|
|
144
257
|
*/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {execSync} from 'node:child_process';
|
|
8
|
+
import {mkdtempSync} from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a gzipped tarball of the repo directory.
|
|
14
|
+
* Excludes node_modules, .git, and other non-essential files.
|
|
15
|
+
*
|
|
16
|
+
* @returns Path to the tarball file (caller must clean up).
|
|
17
|
+
*/
|
|
18
|
+
export async function createRepoTarball(repoPath: string): Promise<string> {
|
|
19
|
+
const tarballPath = path.join(
|
|
20
|
+
mkdtempSync(path.join(os.tmpdir(), 'amodal-deploy-')),
|
|
21
|
+
'repo.tar.gz',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
execSync(
|
|
25
|
+
`tar -czf "${tarballPath}" --exclude=node_modules --exclude=.git --exclude=amodal_packages --exclude=.amodal -C "${repoPath}" .`,
|
|
26
|
+
{stdio: 'pipe', timeout: 30_000},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return tarballPath;
|
|
30
|
+
}
|
package/src/ui/ChatApp.tsx
CHANGED
|
@@ -83,16 +83,17 @@ export const ChatApp: React.FC<ChatAppProps> = ({
|
|
|
83
83
|
}
|
|
84
84
|
}, [resume.messages, resumeSessionId, dispatch]);
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
const canScroll = !state.isStreaming && !state.pendingQuestion && !state.pendingConfirmation && !state.showSessionBrowser;
|
|
86
|
+
// Ctrl+C always active — exits the app
|
|
88
87
|
useInput((_input, key) => {
|
|
89
88
|
if (key.ctrl && _input === 'c') {
|
|
90
89
|
exit();
|
|
91
|
-
return;
|
|
92
90
|
}
|
|
91
|
+
});
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// Scroll keybindings — only active when input bar is NOT showing
|
|
94
|
+
// (i.e., during streaming, prompts, or session browser)
|
|
95
|
+
const isInputBarVisible = !state.isStreaming && !state.pendingQuestion && !state.pendingConfirmation && !state.showSessionBrowser;
|
|
96
|
+
useInput((_input, key) => {
|
|
96
97
|
// j/k line scroll
|
|
97
98
|
if (_input === 'j') {
|
|
98
99
|
scroll.scrollBy(1);
|
|
@@ -107,13 +108,13 @@ export const ChatApp: React.FC<ChatAppProps> = ({
|
|
|
107
108
|
scroll.scrollBy(viewportHeight);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
// Home/End
|
|
111
|
+
// Home/End
|
|
111
112
|
if (key.home) {
|
|
112
113
|
scroll.scrollToTop();
|
|
113
114
|
} else if (key.end) {
|
|
114
115
|
scroll.scrollToBottom();
|
|
115
116
|
}
|
|
116
|
-
});
|
|
117
|
+
}, {isActive: !isInputBarVisible});
|
|
117
118
|
|
|
118
119
|
// Slash command handler
|
|
119
120
|
const handleSlashCommand = useCallback(
|