@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/src/commands/chat.d.ts.map +1 -1
  3. package/dist/src/commands/chat.js +38 -2
  4. package/dist/src/commands/chat.js.map +1 -1
  5. package/dist/src/commands/deploy.d.ts.map +1 -1
  6. package/dist/src/commands/deploy.js +16 -1
  7. package/dist/src/commands/deploy.js.map +1 -1
  8. package/dist/src/commands/dev.d.ts +1 -0
  9. package/dist/src/commands/dev.d.ts.map +1 -1
  10. package/dist/src/commands/dev.js +33 -33
  11. package/dist/src/commands/dev.js.map +1 -1
  12. package/dist/src/commands/index.d.ts.map +1 -1
  13. package/dist/src/commands/index.js +2 -1
  14. package/dist/src/commands/index.js.map +1 -1
  15. package/dist/src/commands/login.d.ts +5 -0
  16. package/dist/src/commands/login.d.ts.map +1 -1
  17. package/dist/src/commands/login.js +70 -4
  18. package/dist/src/commands/login.js.map +1 -1
  19. package/dist/src/commands/validate.d.ts +1 -0
  20. package/dist/src/commands/validate.d.ts.map +1 -1
  21. package/dist/src/commands/validate.js +228 -4
  22. package/dist/src/commands/validate.js.map +1 -1
  23. package/dist/src/shared/connection-preflight.d.ts +52 -0
  24. package/dist/src/shared/connection-preflight.d.ts.map +1 -0
  25. package/dist/src/shared/connection-preflight.js +204 -0
  26. package/dist/src/shared/connection-preflight.js.map +1 -0
  27. package/dist/src/shared/platform-client.d.ts +14 -1
  28. package/dist/src/shared/platform-client.d.ts.map +1 -1
  29. package/dist/src/shared/platform-client.js +103 -2
  30. package/dist/src/shared/platform-client.js.map +1 -1
  31. package/dist/src/shared/tarball.d.ts +13 -0
  32. package/dist/src/shared/tarball.d.ts.map +1 -0
  33. package/dist/src/shared/tarball.js +21 -0
  34. package/dist/src/shared/tarball.js.map +1 -0
  35. package/dist/src/ui/ChatApp.d.ts.map +1 -1
  36. package/dist/src/ui/ChatApp.js +8 -7
  37. package/dist/src/ui/ChatApp.js.map +1 -1
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +4 -4
  40. package/src/commands/chat.ts +38 -2
  41. package/src/commands/deploy.ts +17 -1
  42. package/src/commands/dev.ts +35 -32
  43. package/src/commands/index.ts +2 -1
  44. package/src/commands/login.ts +76 -4
  45. package/src/commands/validate.test.ts +169 -16
  46. package/src/commands/validate.ts +272 -4
  47. package/src/shared/connection-preflight.ts +251 -0
  48. package/src/shared/platform-client.ts +115 -2
  49. package/src/shared/tarball.ts +30 -0
  50. 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
- const resp = await fetch(url, {
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
- * Upload a snapshot and deploy it.
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
+ }
@@ -83,16 +83,17 @@ export const ChatApp: React.FC<ChatAppProps> = ({
83
83
  }
84
84
  }, [resume.messages, resumeSessionId, dispatch]);
85
85
 
86
- // Keyboard navigation for scrolling
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
- if (!canScroll) return;
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 (map to Meta+< and Meta+>)
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(