deploy.sh 1.0.0 → 3.0.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.
Files changed (161) hide show
  1. package/.claude/settings.local.json +36 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/pages.yml +48 -0
  7. package/.oxfmtrc.json +7 -0
  8. package/.oxlintrc.json +11 -0
  9. package/LICENSE +183 -183
  10. package/README.md +99 -11
  11. package/app/actions/deployments.ts +82 -0
  12. package/app/actions/metrics.ts +13 -0
  13. package/app/root.tsx +60 -0
  14. package/app/routes/dashboard/detail/history.tsx +73 -0
  15. package/app/routes/dashboard/detail/layout.tsx +125 -0
  16. package/app/routes/dashboard/detail/logs.tsx +85 -0
  17. package/app/routes/dashboard/detail/overview.tsx +119 -0
  18. package/app/routes/dashboard/detail/requests.tsx +163 -0
  19. package/app/routes/dashboard/detail/resources.tsx +268 -0
  20. package/app/routes/dashboard/detail/shared.tsx +59 -0
  21. package/app/routes/dashboard/index.tsx +360 -0
  22. package/app/routes/dashboard/layout.tsx +30 -0
  23. package/app/routes/docs/architecture.tsx +155 -0
  24. package/app/routes/docs/cli.tsx +122 -0
  25. package/app/routes/docs/deploying.tsx +105 -0
  26. package/app/routes/docs/index.tsx +104 -0
  27. package/app/routes/docs/layout.tsx +58 -0
  28. package/app/routes/home.tsx +134 -0
  29. package/app/routes/root.client.tsx +46 -0
  30. package/app/routes.ts +21 -0
  31. package/app/styles.css +15 -0
  32. package/app/theme.css +134 -0
  33. package/bin/deploy.js +360 -110
  34. package/docs-site/404.html +33 -0
  35. package/docs-site/home.tsx +130 -0
  36. package/docs-site/index.html +35 -0
  37. package/docs-site/layout.tsx +57 -0
  38. package/docs-site/main.tsx +41 -0
  39. package/docs-site/shell.tsx +34 -0
  40. package/docs-site/styles.css +4 -0
  41. package/drizzle.config.js +8 -0
  42. package/examples/docker/Dockerfile +5 -0
  43. package/examples/docker/server.js +18 -0
  44. package/examples/node/package.json +7 -0
  45. package/examples/node/pnpm-lock.yaml +9 -0
  46. package/examples/node/server.js +12 -0
  47. package/examples/static/index.html +48 -0
  48. package/package.json +41 -55
  49. package/public/favicon.ico +0 -0
  50. package/react-router-vite/entry.browser.tsx +49 -0
  51. package/react-router-vite/entry.rsc.single.tsx +7 -0
  52. package/react-router-vite/entry.rsc.tsx +36 -0
  53. package/react-router-vite/entry.ssr.tsx +29 -0
  54. package/react-router-vite/plugin.ts +114 -0
  55. package/react-router-vite/types.d.ts +11 -0
  56. package/react-router.config.ts +5 -0
  57. package/server/api.test.ts +344 -0
  58. package/server/api.ts +445 -0
  59. package/server/docker.ts +268 -0
  60. package/server/index.ts +17 -0
  61. package/server/metrics-collector.ts +29 -0
  62. package/server/schema.ts +56 -0
  63. package/server/store.test.ts +278 -0
  64. package/server/store.ts +398 -0
  65. package/tsconfig.json +21 -0
  66. package/vite.config.ts +45 -0
  67. package/vite.docs.config.ts +31 -0
  68. package/.eslintignore +0 -5
  69. package/.eslintrc +0 -23
  70. package/.travis.yml +0 -9
  71. package/.tryitout +0 -48
  72. package/CHANGELOG.md +0 -56
  73. package/bin/deploy-delete.js +0 -11
  74. package/bin/deploy-deploy.js +0 -31
  75. package/bin/deploy-list.js +0 -32
  76. package/bin/deploy-login.js +0 -39
  77. package/bin/deploy-logout.js +0 -12
  78. package/bin/deploy-logs.js +0 -19
  79. package/bin/deploy-open.js +0 -19
  80. package/bin/deploy-register.js +0 -40
  81. package/bin/deploy-server.js +0 -5
  82. package/bin/deploy-whoami.js +0 -12
  83. package/docs/code/CLI.html +0 -2901
  84. package/docs/code/Deployment.html +0 -2469
  85. package/docs/code/Request.html +0 -906
  86. package/docs/code/User.html +0 -1219
  87. package/docs/code/classifier.js.html +0 -121
  88. package/docs/code/deploy.js.html +0 -122
  89. package/docs/code/fonts/OpenSans-Bold-webfont.eot +0 -0
  90. package/docs/code/fonts/OpenSans-Bold-webfont.svg +0 -1830
  91. package/docs/code/fonts/OpenSans-Bold-webfont.woff +0 -0
  92. package/docs/code/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  93. package/docs/code/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  94. package/docs/code/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  95. package/docs/code/fonts/OpenSans-Italic-webfont.eot +0 -0
  96. package/docs/code/fonts/OpenSans-Italic-webfont.svg +0 -1830
  97. package/docs/code/fonts/OpenSans-Italic-webfont.woff +0 -0
  98. package/docs/code/fonts/OpenSans-Light-webfont.eot +0 -0
  99. package/docs/code/fonts/OpenSans-Light-webfont.svg +0 -1831
  100. package/docs/code/fonts/OpenSans-Light-webfont.woff +0 -0
  101. package/docs/code/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  102. package/docs/code/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  103. package/docs/code/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  104. package/docs/code/fonts/OpenSans-Regular-webfont.eot +0 -0
  105. package/docs/code/fonts/OpenSans-Regular-webfont.svg +0 -1831
  106. package/docs/code/fonts/OpenSans-Regular-webfont.woff +0 -0
  107. package/docs/code/fonts/OpenSans-Semibold-webfont.eot +0 -0
  108. package/docs/code/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  109. package/docs/code/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  110. package/docs/code/fonts/OpenSans-Semibold-webfont.woff +0 -0
  111. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  112. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  113. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  114. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  115. package/docs/code/helpers_cli.js.html +0 -315
  116. package/docs/code/helpers_util.js.html +0 -194
  117. package/docs/code/index.html +0 -66
  118. package/docs/code/models_deployment.js.html +0 -515
  119. package/docs/code/models_request.js.html +0 -158
  120. package/docs/code/models_user.js.html +0 -198
  121. package/docs/code/module-lib_classifier.html +0 -246
  122. package/docs/code/module-lib_deploy.html +0 -350
  123. package/docs/code/module-lib_helpers_util.html +0 -707
  124. package/docs/code/scripts/linenumber.js +0 -25
  125. package/docs/code/scripts/prettify/Apache-License-2.0.txt +0 -202
  126. package/docs/code/scripts/prettify/lang-css.js +0 -2
  127. package/docs/code/scripts/prettify/prettify.js +0 -28
  128. package/docs/code/styles/jsdoc-default.css +0 -692
  129. package/docs/code/styles/prettify-jsdoc.css +0 -111
  130. package/docs/code/styles/prettify-tomorrow.css +0 -132
  131. package/docs/example.gif +0 -0
  132. package/docs/example.mov +0 -0
  133. package/docs/index.html +0 -4463
  134. package/docs/logo.png +0 -0
  135. package/docs/logo.pxm +0 -0
  136. package/docs/logo@2x.png +0 -0
  137. package/docs/main.css +0 -162
  138. package/docs/main.js +0 -53
  139. package/index.js +0 -55
  140. package/jsdoc.json +0 -27
  141. package/lib/classifier.js +0 -61
  142. package/lib/deploy.js +0 -62
  143. package/lib/helpers/cli.js +0 -255
  144. package/lib/helpers/util.js +0 -134
  145. package/lib/models/deployment.js +0 -455
  146. package/lib/models/request.js +0 -98
  147. package/lib/models/user.js +0 -138
  148. package/lib/server.js +0 -165
  149. package/lib/static/not-found.html +0 -30
  150. package/lib/static/page-could-not-load.html +0 -30
  151. package/lib/static/static-server.js +0 -69
  152. package/test/fixtures/docker/Dockerfile +0 -5
  153. package/test/fixtures/docker/index.js +0 -12
  154. package/test/fixtures/node/index.js +0 -8
  155. package/test/fixtures/node/package.json +0 -15
  156. package/test/fixtures/static/index.html +0 -14
  157. package/test/fixtures/static/main.css +0 -7
  158. package/test/fixtures/static/out.gifcd +0 -0
  159. package/test/fixtures/unknown/.gitkeep +0 -0
  160. package/test/lib/classifier.js +0 -51
  161. package/test/lib/helpers/util.js +0 -47
@@ -0,0 +1,344 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { type ChildProcess, spawn } from 'node:child_process';
4
+ import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { type AddressInfo, createServer } from 'node:net';
8
+
9
+ // Find an available port
10
+ function getPort(): Promise<number> {
11
+ return new Promise((resolve, reject) => {
12
+ const srv = createServer();
13
+ srv.listen(0, () => {
14
+ const { port } = srv.address() as AddressInfo;
15
+ srv.close(() => resolve(port));
16
+ });
17
+ srv.on('error', reject);
18
+ });
19
+ }
20
+
21
+ // Start the API server as a child process in a temp directory
22
+ function startServer(port: number, cwd: string): Promise<ChildProcess> {
23
+ return new Promise((resolve, reject) => {
24
+ const serverPath = join(process.cwd(), 'server', 'index.ts');
25
+ const child = spawn('node', [serverPath], {
26
+ env: { ...process.env, PORT: String(port) },
27
+ cwd,
28
+ stdio: ['pipe', 'pipe', 'pipe'],
29
+ });
30
+
31
+ let started = false;
32
+ const timeout = setTimeout(() => {
33
+ if (!started) {
34
+ child.kill();
35
+ reject(new Error('Server did not start within 5s'));
36
+ }
37
+ }, 5000);
38
+
39
+ child.stdout!.on('data', (data: Buffer) => {
40
+ if (!started && data.toString().includes('running on')) {
41
+ started = true;
42
+ clearTimeout(timeout);
43
+ resolve(child);
44
+ }
45
+ });
46
+
47
+ child.stderr!.on('data', (data: Buffer) => {
48
+ if (!started) {
49
+ clearTimeout(timeout);
50
+ reject(new Error(`Server error: ${data}`));
51
+ }
52
+ });
53
+
54
+ child.on('error', (err) => {
55
+ clearTimeout(timeout);
56
+ reject(err);
57
+ });
58
+ });
59
+ }
60
+
61
+ // HTTP helper
62
+ async function req(port: number, path: string, options: RequestInit = {}) {
63
+ const res = await fetch(`http://localhost:${port}${path}`, options);
64
+ const text = await res.text();
65
+ let body;
66
+ try {
67
+ body = JSON.parse(text);
68
+ } catch {
69
+ body = text;
70
+ }
71
+ return { status: res.status, body, headers: res.headers };
72
+ }
73
+
74
+ function authHeaders(username: string, token: string) {
75
+ return {
76
+ 'x-deploy-username': username,
77
+ 'x-deploy-token': token,
78
+ };
79
+ }
80
+
81
+ describe('API – auth flow', () => {
82
+ let server: ChildProcess;
83
+ let port: number;
84
+ let tempDir: string;
85
+
86
+ before(async () => {
87
+ port = await getPort();
88
+ tempDir = mkdtempSync(join(tmpdir(), 'deploy-sh-api-'));
89
+ server = await startServer(port, tempDir);
90
+ });
91
+
92
+ after(() => {
93
+ server?.kill();
94
+ rmSync(tempDir, { recursive: true, force: true });
95
+ });
96
+
97
+ it('POST /register creates a user and returns 201', async () => {
98
+ const { status, body } = await req(port, '/register', {
99
+ method: 'POST',
100
+ headers: { 'Content-Type': 'application/json' },
101
+ body: JSON.stringify({ username: 'alice', password: 'pass123' }),
102
+ });
103
+ assert.equal(status, 201);
104
+ assert.ok(body.token);
105
+ assert.equal(typeof body.token, 'string');
106
+ });
107
+
108
+ it('POST /register rejects duplicate username', async () => {
109
+ const { status, body } = await req(port, '/register', {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({ username: 'alice', password: 'other' }),
113
+ });
114
+ assert.equal(status, 409);
115
+ assert.ok(body.error);
116
+ });
117
+
118
+ it('POST /register rejects missing fields', async () => {
119
+ const { status } = await req(port, '/register', {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({ username: 'bob' }),
123
+ });
124
+ assert.equal(status, 400);
125
+ });
126
+
127
+ it('POST /login returns token for valid credentials', async () => {
128
+ // Register first
129
+ await req(port, '/register', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify({ username: 'logintest', password: 'secret' }),
133
+ });
134
+ const { status, body } = await req(port, '/login', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ username: 'logintest', password: 'secret' }),
138
+ });
139
+ assert.equal(status, 200);
140
+ assert.ok(body.token);
141
+ });
142
+
143
+ it('POST /login rejects wrong password', async () => {
144
+ const { status, body } = await req(port, '/login', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({ username: 'logintest', password: 'wrong' }),
148
+ });
149
+ assert.equal(status, 401);
150
+ assert.ok(body.error);
151
+ });
152
+
153
+ it('GET /api/user returns user info with valid auth', async () => {
154
+ const reg = await req(port, '/register', {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({ username: 'userinfo', password: 'pass' }),
158
+ });
159
+ const { status, body } = await req(port, '/api/user', {
160
+ headers: authHeaders('userinfo', reg.body.token),
161
+ });
162
+ assert.equal(status, 200);
163
+ assert.equal(body.username, 'userinfo');
164
+ assert.ok(body.createdAt);
165
+ });
166
+
167
+ it('GET /api/user returns 401 without auth', async () => {
168
+ const { status } = await req(port, '/api/user');
169
+ assert.equal(status, 401);
170
+ });
171
+
172
+ it('GET /api/logout invalidates the token', async () => {
173
+ const reg = await req(port, '/register', {
174
+ method: 'POST',
175
+ headers: { 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({ username: 'logouttest', password: 'pass' }),
177
+ });
178
+ const token = reg.body.token;
179
+
180
+ // Logout
181
+ const { status } = await req(port, '/api/logout', {
182
+ headers: authHeaders('logouttest', token),
183
+ });
184
+ assert.equal(status, 200);
185
+
186
+ // Token should now be invalid
187
+ const { status: afterStatus } = await req(port, '/api/user', {
188
+ headers: authHeaders('logouttest', token),
189
+ });
190
+ assert.equal(afterStatus, 401);
191
+ });
192
+ });
193
+
194
+ describe('API – deployments', () => {
195
+ let server: ChildProcess;
196
+ let port: number;
197
+ let tempDir: string;
198
+ let token: string;
199
+
200
+ before(async () => {
201
+ port = await getPort();
202
+ tempDir = mkdtempSync(join(tmpdir(), 'deploy-sh-api-'));
203
+ server = await startServer(port, tempDir);
204
+
205
+ // Register a user for deployment tests
206
+ const reg = await req(port, '/register', {
207
+ method: 'POST',
208
+ headers: { 'Content-Type': 'application/json' },
209
+ body: JSON.stringify({ username: 'deployer', password: 'pass' }),
210
+ });
211
+ token = reg.body.token;
212
+ });
213
+
214
+ after(() => {
215
+ server?.kill();
216
+ rmSync(tempDir, { recursive: true, force: true });
217
+ });
218
+
219
+ it('GET /api/deployments returns empty array for new user', async () => {
220
+ const { status, body } = await req(port, '/api/deployments', {
221
+ headers: authHeaders('deployer', token),
222
+ });
223
+ assert.equal(status, 200);
224
+ assert.deepEqual(body, []);
225
+ });
226
+
227
+ it('GET /api/deployments/:name returns 404 for non-existent', async () => {
228
+ const { status } = await req(port, '/api/deployments/nope', {
229
+ headers: authHeaders('deployer', token),
230
+ });
231
+ assert.equal(status, 404);
232
+ });
233
+
234
+ it('DELETE /api/deployments/:name returns 404 for non-existent', async () => {
235
+ const { status } = await req(port, '/api/deployments/nope', {
236
+ method: 'DELETE',
237
+ headers: authHeaders('deployer', token),
238
+ });
239
+ assert.equal(status, 404);
240
+ });
241
+
242
+ it('GET /api/deployments/:name/inspect returns 404 for non-existent', async () => {
243
+ const { status } = await req(port, '/api/deployments/nope/inspect', {
244
+ headers: authHeaders('deployer', token),
245
+ });
246
+ assert.equal(status, 404);
247
+ });
248
+
249
+ it('GET /api/deployments/:name/stats returns 404 for non-existent', async () => {
250
+ const { status } = await req(port, '/api/deployments/nope/stats', {
251
+ headers: authHeaders('deployer', token),
252
+ });
253
+ assert.equal(status, 404);
254
+ });
255
+
256
+ it('POST /api/deployments/:name/restart returns 404 for non-existent', async () => {
257
+ const { status } = await req(port, '/api/deployments/nope/restart', {
258
+ method: 'POST',
259
+ headers: authHeaders('deployer', token),
260
+ });
261
+ assert.equal(status, 404);
262
+ });
263
+
264
+ it('GET /api/deployments/:name/history returns 404 for non-existent', async () => {
265
+ const { status } = await req(port, '/api/deployments/nope/history', {
266
+ headers: authHeaders('deployer', token),
267
+ });
268
+ assert.equal(status, 404);
269
+ });
270
+ });
271
+
272
+ describe('API – upload validation', () => {
273
+ let server: ChildProcess;
274
+ let port: number;
275
+ let tempDir: string;
276
+ let token: string;
277
+
278
+ before(async () => {
279
+ port = await getPort();
280
+ tempDir = mkdtempSync(join(tmpdir(), 'deploy-sh-api-'));
281
+ server = await startServer(port, tempDir);
282
+
283
+ const reg = await req(port, '/register', {
284
+ method: 'POST',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({ username: 'uploader', password: 'pass' }),
287
+ });
288
+ token = reg.body.token;
289
+ });
290
+
291
+ after(() => {
292
+ server?.kill();
293
+ rmSync(tempDir, { recursive: true, force: true });
294
+ });
295
+
296
+ it('POST /upload returns 401 without auth', async () => {
297
+ const { status } = await req(port, '/upload', { method: 'POST' });
298
+ assert.equal(status, 401);
299
+ });
300
+
301
+ it('POST /upload returns 400 with no file', async () => {
302
+ const boundary = '----TestBoundary';
303
+ const body = `--${boundary}\r\nContent-Disposition: form-data; name="name"\r\n\r\ntestapp\r\n--${boundary}--\r\n`;
304
+ const { status } = await req(port, '/upload', {
305
+ method: 'POST',
306
+ headers: {
307
+ ...authHeaders('uploader', token),
308
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
309
+ },
310
+ body,
311
+ });
312
+ assert.equal(status, 400);
313
+ });
314
+ });
315
+
316
+ describe('API – CORS and 404', () => {
317
+ let server: ChildProcess;
318
+ let port: number;
319
+ let tempDir: string;
320
+
321
+ before(async () => {
322
+ port = await getPort();
323
+ tempDir = mkdtempSync(join(tmpdir(), 'deploy-sh-api-'));
324
+ server = await startServer(port, tempDir);
325
+ });
326
+
327
+ after(() => {
328
+ server?.kill();
329
+ rmSync(tempDir, { recursive: true, force: true });
330
+ });
331
+
332
+ it('OPTIONS returns 204 with CORS headers', async () => {
333
+ const { status, headers } = await req(port, '/', { method: 'OPTIONS' });
334
+ assert.equal(status, 204);
335
+ assert.equal(headers.get('access-control-allow-origin'), '*');
336
+ assert.ok(headers.get('access-control-allow-methods'));
337
+ });
338
+
339
+ it('GET /nonexistent returns 404', async () => {
340
+ const { status, body } = await req(port, '/nonexistent');
341
+ assert.equal(status, 404);
342
+ assert.ok(body.error);
343
+ });
344
+ });