@flowershow/publish 0.4.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 (55) hide show
  1. package/README.md +332 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +86 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/lib/api-client.d.ts +76 -0
  7. package/dist/lib/api-client.d.ts.map +1 -0
  8. package/dist/lib/api-client.js +102 -0
  9. package/dist/lib/api-client.js.map +1 -0
  10. package/dist/lib/auth.d.ts +19 -0
  11. package/dist/lib/auth.d.ts.map +1 -0
  12. package/dist/lib/auth.js +128 -0
  13. package/dist/lib/auth.js.map +1 -0
  14. package/dist/lib/commands/auth-login.d.ts +2 -0
  15. package/dist/lib/commands/auth-login.d.ts.map +1 -0
  16. package/dist/lib/commands/auth-login.js +54 -0
  17. package/dist/lib/commands/auth-login.js.map +1 -0
  18. package/dist/lib/commands/auth-logout.d.ts +2 -0
  19. package/dist/lib/commands/auth-logout.d.ts.map +1 -0
  20. package/dist/lib/commands/auth-logout.js +26 -0
  21. package/dist/lib/commands/auth-logout.js.map +1 -0
  22. package/dist/lib/commands/auth-status.d.ts +2 -0
  23. package/dist/lib/commands/auth-status.d.ts.map +1 -0
  24. package/dist/lib/commands/auth-status.js +35 -0
  25. package/dist/lib/commands/auth-status.js.map +1 -0
  26. package/dist/lib/commands/delete.d.ts +2 -0
  27. package/dist/lib/commands/delete.d.ts.map +1 -0
  28. package/dist/lib/commands/delete.js +63 -0
  29. package/dist/lib/commands/delete.js.map +1 -0
  30. package/dist/lib/commands/list.d.ts +2 -0
  31. package/dist/lib/commands/list.d.ts.map +1 -0
  32. package/dist/lib/commands/list.js +41 -0
  33. package/dist/lib/commands/list.js.map +1 -0
  34. package/dist/lib/commands/publish.d.ts +2 -0
  35. package/dist/lib/commands/publish.d.ts.map +1 -0
  36. package/dist/lib/commands/publish.js +128 -0
  37. package/dist/lib/commands/publish.js.map +1 -0
  38. package/dist/lib/commands/sync.d.ts +6 -0
  39. package/dist/lib/commands/sync.d.ts.map +1 -0
  40. package/dist/lib/commands/sync.js +162 -0
  41. package/dist/lib/commands/sync.js.map +1 -0
  42. package/dist/lib/const.d.ts +4 -0
  43. package/dist/lib/const.d.ts.map +1 -0
  44. package/dist/lib/const.js +4 -0
  45. package/dist/lib/const.js.map +1 -0
  46. package/dist/lib/files.d.ts +13 -0
  47. package/dist/lib/files.d.ts.map +1 -0
  48. package/dist/lib/files.js +159 -0
  49. package/dist/lib/files.js.map +1 -0
  50. package/dist/lib/utils.d.ts +22 -0
  51. package/dist/lib/utils.d.ts.map +1 -0
  52. package/dist/lib/utils.js +90 -0
  53. package/dist/lib/utils.js.map +1 -0
  54. package/dist/package.json +55 -0
  55. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # FlowerShow CLI (Beta 🚧)
2
+
3
+ A CLI tool for publishing Markdown files and folders directly to FlowerShow with OAuth authentication.
4
+
5
+ > **Note:** This package was previously named `flowershow` (and `flowershow-publish`). If you're migrating from the old package, see the [Migration Guide](MIGRATION.md).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g @flowershow/publish@latest
11
+ ```
12
+
13
+ Then use the `publish` command anywhere:
14
+
15
+ ```bash
16
+ publish auth login
17
+ publish ./my-notes
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Authenticate
23
+
24
+ Before using any commands, you must authenticate:
25
+
26
+ ```bash
27
+ publish auth login
28
+ ```
29
+
30
+ This will:
31
+
32
+ 1. Display a URL and verification code
33
+ 2. Open your browser to authorize the CLI
34
+ 3. Store your authentication token locally
35
+
36
+ ### 2. Publish Your Content
37
+
38
+ ```bash
39
+ # Publish a folder
40
+ publish ./my-notes
41
+
42
+ # Publish a single file
43
+ publish ./my-note.md
44
+ ```
45
+
46
+ ### 3. Sync after changes
47
+
48
+ ```bash
49
+ # Sync a folder site
50
+ publish sync ./my-notes
51
+
52
+ # Sync a single file site
53
+ publish sync ./my-note.md
54
+ ```
55
+
56
+ ## Commands
57
+
58
+ ### Authentication
59
+
60
+ #### `publish auth login`
61
+
62
+ Authenticate with FlowerShow via browser OAuth flow.
63
+
64
+ ```bash
65
+ publish auth login
66
+ ```
67
+
68
+ #### `publish auth status`
69
+
70
+ Check your current authentication status.
71
+
72
+ ```bash
73
+ publish auth status
74
+ ```
75
+
76
+ #### `publish auth logout`
77
+
78
+ Remove your stored authentication token.
79
+
80
+ ```bash
81
+ publish auth logout
82
+ ```
83
+
84
+ See [Authentication Documentation](docs/authentication.md) for detailed information.
85
+
86
+ ### Publishing
87
+
88
+ #### `publish <path> [morePaths...] [options]`
89
+
90
+ Publish files or folders to FlowerShow.
91
+
92
+ **Options:**
93
+
94
+ - `--overwrite` - Overwrite existing site if it already exists
95
+ - `--name <siteName>` - Custom name for the site (defaults to file/folder name). Note: If you use it, you need to pass it also to the `sync` command later on, so that Flowershow knows content of which site you're trying to sync.
96
+
97
+ **Examples:**
98
+
99
+ ```bash
100
+ # Publish a single markdown file
101
+ publish ./my-note.md
102
+
103
+ # Publish multiple files
104
+ publish ./intro.md ./chapter1.md ./chapter2.md
105
+
106
+ # Publish a folder
107
+ publish ./my-notes
108
+
109
+ # Overwrite an existing site
110
+ publish ./my-notes --overwrite
111
+
112
+ # Publish with a custom site name
113
+ publish ./my-notes --name my-custom-site
114
+
115
+ # Combine options
116
+ publish ./my-notes --name my-custom-site --overwrite
117
+ ```
118
+
119
+ **What happens:**
120
+
121
+ 1. Files are discovered and filtered (ignores `.git`, `node_modules`, etc.; also supports `.gitignore` and will ignore paths listed there)
122
+ 2. Project name is derived from the first file name or the folder name
123
+ 3. Site is created via the FlowerShow API
124
+ 4. Presigned URLs are obtained for secure file uploads
125
+ 5. Files are uploaded directly to Cloudflare R2 storage
126
+ 6. CLI waits for markdown files to be processed
127
+ 7. Site URL is displayed
128
+
129
+ **Single file behavior:**
130
+
131
+ - Filename becomes the project name (e.g. `publish about.md` will create a site named `about`)
132
+ - File is saved as `README.md` (or `README.mdx` depending on the original file extension)
133
+ - Site accessible at `/@{username}/{filename}` (e.g. `/@johndoe/about`)
134
+
135
+ **Multiple files behavior:**
136
+
137
+ - First filename becomes the project name (e.g. `publish about.md team.md abc.md` will create a site named `about`)
138
+ - First file is saved as `README.md` (or `README.mdx`)
139
+ - Subsequent files keep their original names
140
+ - Site accessible at `/@{username}/{first-filename}` (e.g. `/@johndoe/about`)
141
+
142
+ **Folder behavior:**
143
+
144
+ - Folder name becomes the project name (e.g. `publish my-digital-garden/blog` will create a site named `blog`)
145
+ - All files maintain their relative paths
146
+ - Site accessible at `/@{username}/{foldername}` (e.g. `/@johndoe/blog`)
147
+
148
+ #### `publish sync <path> [options]`
149
+
150
+ Sync changes to an existing published site. Only uploads new or modified files, and deletes files that no longer exist locally.
151
+
152
+ **Options:**
153
+
154
+ - `--name <siteName>` - Specify site name if different from folder name
155
+ - `--dry-run` - Show what would be synced without making changes
156
+ - `--verbose` - Show detailed list of all files in each category
157
+
158
+ **Examples:**
159
+
160
+ ```bash
161
+ # Sync changes to a folder
162
+ publish sync ./my-notes
163
+
164
+ # Preview changes without syncing
165
+ publish sync ./my-notes --dry-run
166
+
167
+ # Show detailed file lists including unchanged files
168
+ publish sync ./my-notes --verbose
169
+
170
+ # Sync to a specific site name
171
+ publish sync ./my-notes --name my-custom-site
172
+
173
+ # Combine options
174
+ publish sync ./my-notes --dry-run --verbose
175
+ ```
176
+
177
+ **What happens:**
178
+
179
+ 1. Files are discovered and SHA hashes calculated
180
+ 2. File list is sent to the API for comparison
181
+ 3. API compares with existing files and determines:
182
+ - New files (not in database)
183
+ - Modified files (different SHA hash)
184
+ - Deleted files (in database but not in request)
185
+ - Unchanged files (same SHA hash)
186
+ 4. Sync summary is displayed
187
+ 5. Only new/modified files are uploaded
188
+ 6. Deleted files are removed by the API
189
+ 7. CLI waits for markdown files to be processed
190
+ 8. Site URL is displayed
191
+
192
+ **When to use sync vs publish:**
193
+
194
+ - **Use `publish`** for initial site creation or complete site replacement
195
+ - **Use `sync`** for updates to existing sites
196
+
197
+ ### Site Management
198
+
199
+ #### `publish list`
200
+
201
+ List all sites published by your authenticated user.
202
+
203
+ ```bash
204
+ publish list
205
+ ```
206
+
207
+ Shows site names, URLs, and timestamps.
208
+
209
+ #### `publish delete <project-name>`
210
+
211
+ Delete a site and all its files.
212
+
213
+ ```bash
214
+ publish delete my-notes
215
+ ```
216
+
217
+ Removes the site and all its files via the FlowerShow API.
218
+
219
+ ## File Filtering
220
+
221
+ The CLI automatically ignores common non-content files and directories:
222
+
223
+ - `.git/`, `node_modules/`, `.cache/`, `dist/`, `build/`
224
+ - `.DS_Store`, `Thumbs.db`
225
+ - `.env*`, `*.log`
226
+ - `.next/`, `.vercel/`, `.turbo/`
227
+
228
+ If `.gitignore` file is present in the published folder, the Flowershow CLI will also ignore files matched by it.
229
+
230
+ ## Site URLs
231
+
232
+ All CLI-published sites are accessible at:
233
+
234
+ ```
235
+ https://my.flowershow.app/@{username}/{project-name}
236
+ ```
237
+
238
+ Where `{username}` is your authenticated username.
239
+
240
+ ## Troubleshooting
241
+
242
+ ### "You must be authenticated to use this command"
243
+
244
+ Run `publish auth login` to authenticate.
245
+
246
+ ### "Authentication token is invalid or expired"
247
+
248
+ Your token may have been revoked. Re-authenticate:
249
+
250
+ ```bash
251
+ publish auth login
252
+ ```
253
+
254
+ ### "Site already exists"
255
+
256
+ A site with that name already exists. You can:
257
+
258
+ - Use the `--overwrite` flag: `publish <path> --overwrite`
259
+ - Delete it first: `publish delete <name>`
260
+ - Rename your file/folder
261
+ - Use `publish list` to see all existing sites
262
+ - **Or use `publish sync`** to update an existing site incrementally
263
+
264
+ ### "Site not found" (when using sync)
265
+
266
+ The sync command requires the site to already exist. If you get this error:
267
+
268
+ - Use `publish` to create the site first
269
+ - Check the site name with `publish list`
270
+ - Specify the correct site name with `--name`
271
+
272
+ ### Files still processing after timeout
273
+
274
+ The site is live, but some pages may not be ready yet. The Cloudflare worker processes files asynchronously. Check your site again in a moment.
275
+
276
+ ## Architecture
277
+
278
+ All CLI commands communicate with the Flowershow API:
279
+
280
+ - **Authentication**: OAuth device flow endpoints
281
+ - **Site Management**: Create, list, and delete sites
282
+ - **File Upload**: Presigned URL generation and status polling
283
+ - **User Info**: Retrieve authenticated user details
284
+
285
+ ### Security
286
+
287
+ - **Token Storage**: Authentication tokens are stored in `~/.flowershow/token.json`
288
+ - **Token Format**: CLI tokens use the `fs_cli_` prefix
289
+ - **Token Expiration**: Tokens do not expire by default
290
+ - **Token Revocation**: Revoke tokens from the [Flowershow dashboard](https://cloud.flowershow.app/tokens) or via `flowershow auth logout`
291
+ - **Secure Uploads**: Files are uploaded using time-limited presigned URLs
292
+ - **No Credentials**: CLI never stores database or storage credentials
293
+
294
+ ## Development
295
+
296
+ ### Setup
297
+
298
+ 1. **Clone and install dependencies:**
299
+
300
+ ```bash
301
+ cd cli
302
+ pnpm install
303
+ ```
304
+
305
+ 2. **Configure environment:**
306
+
307
+ Use local or other non-production API and publish URLs.
308
+
309
+ ```bash
310
+ cp .env.example .env
311
+ ```
312
+
313
+ ```bash
314
+ API_URL="http://cloud.localhost:3000"
315
+ APP_URL="http://my.localhost:3000"
316
+ ```
317
+
318
+ 3. **Run commands:**
319
+
320
+ ```bash
321
+ pnpm dev auth login
322
+ pnpm dev ./my-notes
323
+ pnpm dev sync ./my-notes
324
+ ```
325
+
326
+ You can also build the project, link it globally and use it as you normally would the npm-installed version:
327
+
328
+ ```bash
329
+ pnpm build
330
+ npm link
331
+ publish ...
332
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import { publishCommand } from "./lib/commands/publish.js";
5
+ import { syncCommand } from "./lib/commands/sync.js";
6
+ import { listCommand } from "./lib/commands/list.js";
7
+ import { deleteCommand } from "./lib/commands/delete.js";
8
+ import { authLoginCommand } from "./lib/commands/auth-login.js";
9
+ import { authLogoutCommand } from "./lib/commands/auth-logout.js";
10
+ import { authStatusCommand } from "./lib/commands/auth-status.js";
11
+ import packageJson from "./package.json" with { type: "json" };
12
+ const program = new Command();
13
+ program
14
+ .name("publish")
15
+ .description("CLI tool for publishing to FlowerShow")
16
+ .version(packageJson.version);
17
+ program
18
+ .argument("[path]", "File or folder to publish")
19
+ .argument("[morePaths...]", "Additional files to publish")
20
+ .option("--overwrite", "Overwrite existing site if it already exists")
21
+ .option("--name <siteName>", "Custom name for the site")
22
+ .action(async (path, morePaths, options) => {
23
+ if (!path) {
24
+ program.help();
25
+ return;
26
+ }
27
+ console.log(chalk.bold("\n💐 FlowerShow CLI - Publish\n"));
28
+ const paths = [path, ...morePaths];
29
+ await publishCommand(paths, options.overwrite || false, options.name);
30
+ });
31
+ const auth = program
32
+ .command("auth")
33
+ .description("Manage authentication")
34
+ .action(() => {
35
+ auth.help();
36
+ });
37
+ auth
38
+ .command("login")
39
+ .description("Authenticate with FlowerShow via browser")
40
+ .action(async () => {
41
+ console.log(chalk.bold("\n💐 FlowerShow CLI - Authentication\n"));
42
+ await authLoginCommand();
43
+ });
44
+ auth
45
+ .command("logout")
46
+ .description("Remove stored authentication token")
47
+ .action(async () => {
48
+ console.log(chalk.bold("\n💐 FlowerShow CLI - Logout\n"));
49
+ await authLogoutCommand();
50
+ });
51
+ auth
52
+ .command("status")
53
+ .description("Check authentication status")
54
+ .action(async () => {
55
+ console.log(chalk.bold("\n💐 FlowerShow CLI - Auth Status\n"));
56
+ await authStatusCommand();
57
+ });
58
+ program
59
+ .command("sync <path>")
60
+ .description("Sync changes to an existing published site")
61
+ .option("--name <siteName>", "Specify site name if different from folder name")
62
+ .option("--dry-run", "Show what would be synced without making changes")
63
+ .option("--verbose", "Show detailed list of all files in each category")
64
+ .action(async (path, options) => {
65
+ console.log(chalk.bold("\n💐 FlowerShow CLI - Sync\n"));
66
+ await syncCommand(path, options);
67
+ });
68
+ program
69
+ .command("list")
70
+ .description("List all published sites")
71
+ .action(async () => {
72
+ console.log(chalk.bold("\n💐 FlowerShow CLI - List Sites\n"));
73
+ await listCommand();
74
+ });
75
+ program
76
+ .command("delete <project-name>")
77
+ .description("Delete a published site")
78
+ .action(async (projectName) => {
79
+ console.log(chalk.bold("\n💐 FlowerShow CLI - Delete Site\n"));
80
+ await deleteCommand(projectName);
81
+ });
82
+ if (process.argv.length === 2) {
83
+ program.help();
84
+ }
85
+ program.parse();
86
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ interface Site {
2
+ id: string;
3
+ projectName: string;
4
+ url: string;
5
+ userId: string;
6
+ createdAt: string;
7
+ updatedAt?: string;
8
+ fileCount?: number;
9
+ totalSize?: number;
10
+ plan?: "FREE" | "PREMIUM";
11
+ }
12
+ interface CreateSiteResponse {
13
+ site: Site;
14
+ }
15
+ interface GetSitesResponse {
16
+ sites: Site[];
17
+ total: number;
18
+ }
19
+ interface GetSiteResponse {
20
+ site: Site;
21
+ }
22
+ interface DeleteSiteResponse {
23
+ success: boolean;
24
+ message: string;
25
+ deletedFiles: number;
26
+ }
27
+ interface FileMetadata {
28
+ path: string;
29
+ size: number;
30
+ sha: string;
31
+ }
32
+ interface UploadUrl {
33
+ path: string;
34
+ uploadUrl: string;
35
+ blobId: string;
36
+ contentType: string;
37
+ }
38
+ interface SyncFilesResponse {
39
+ uploadUrls: UploadUrl[];
40
+ deleted: string[];
41
+ unchanged: string[];
42
+ summary: {
43
+ toUpload: number;
44
+ toDelete: number;
45
+ unchanged: number;
46
+ };
47
+ expiresIn: number;
48
+ dryRun?: boolean;
49
+ }
50
+ interface BlobStatus {
51
+ path: string;
52
+ syncStatus: "PENDING" | "SUCCESS" | "ERROR";
53
+ syncError?: string;
54
+ }
55
+ interface SiteStatusResponse {
56
+ siteId: string;
57
+ status: string;
58
+ files: {
59
+ total: number;
60
+ pending: number;
61
+ success: number;
62
+ failed: number;
63
+ };
64
+ blobs: BlobStatus[];
65
+ }
66
+ export declare function apiRequest(endpoint: string, options?: RequestInit): Promise<Response>;
67
+ export declare function createSite(projectName: string, overwrite?: boolean): Promise<CreateSiteResponse>;
68
+ export declare function getSites(): Promise<GetSitesResponse>;
69
+ export declare function getSiteById(siteId: string): Promise<GetSiteResponse>;
70
+ export declare function getSiteByName(siteName: string): Promise<GetSiteResponse | null>;
71
+ export declare function deleteSite(siteId: string): Promise<DeleteSiteResponse>;
72
+ export declare function syncFiles(siteId: string, files: FileMetadata[], dryRun?: boolean): Promise<SyncFilesResponse>;
73
+ export declare function uploadToR2(uploadUrl: string, content: Buffer, contentType: string): Promise<boolean>;
74
+ export declare function getSiteStatus(siteId: string): Promise<SiteStatusResponse>;
75
+ export {};
76
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../lib/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import { getAuthHeaders } from "./auth.js";
2
+ import { API_URL } from "./const.js";
3
+ export async function apiRequest(endpoint, options = {}) {
4
+ const authHeaders = getAuthHeaders();
5
+ const url = `${API_URL}${endpoint}`;
6
+ const headers = {
7
+ ...options.headers,
8
+ ...(authHeaders || {}),
9
+ };
10
+ return fetch(url, {
11
+ ...options,
12
+ headers,
13
+ });
14
+ }
15
+ export async function createSite(projectName, overwrite = false) {
16
+ const response = await apiRequest("/api/cli/site", {
17
+ method: "POST",
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ },
21
+ body: JSON.stringify({ projectName, overwrite }),
22
+ });
23
+ if (!response.ok) {
24
+ const error = (await response.json().catch(() => ({})));
25
+ throw new Error(error.message || `Failed to create site: ${response.statusText}`);
26
+ }
27
+ return (await response.json());
28
+ }
29
+ export async function getSites() {
30
+ const response = await apiRequest("/api/cli/site/get-all");
31
+ if (!response.ok) {
32
+ const error = (await response.json().catch(() => ({})));
33
+ throw new Error(error.message || `Failed to fetch sites: ${response.statusText}`);
34
+ }
35
+ return (await response.json());
36
+ }
37
+ export async function getSiteById(siteId) {
38
+ const response = await apiRequest(`/api/cli/site/${siteId}`);
39
+ if (!response.ok) {
40
+ const error = (await response.json().catch(() => ({})));
41
+ throw new Error(error.message || `Failed to fetch site: ${response.statusText}`);
42
+ }
43
+ return (await response.json());
44
+ }
45
+ export async function getSiteByName(siteName) {
46
+ const response = await apiRequest(`/api/cli/site/by-name/${siteName}`);
47
+ if (!response.ok) {
48
+ if (response.status === 404) {
49
+ return null;
50
+ }
51
+ const error = (await response.json().catch(() => ({})));
52
+ throw new Error(error.message || `Failed to fetch site: ${response.statusText}`);
53
+ }
54
+ return (await response.json());
55
+ }
56
+ export async function deleteSite(siteId) {
57
+ const response = await apiRequest(`/api/cli/site/${siteId}`, {
58
+ method: "DELETE",
59
+ });
60
+ if (!response.ok) {
61
+ const error = (await response.json().catch(() => ({})));
62
+ throw new Error(error.message || `Failed to delete site: ${response.statusText}`);
63
+ }
64
+ return (await response.json());
65
+ }
66
+ export async function syncFiles(siteId, files, dryRun = false) {
67
+ const url = `/api/cli/site/${siteId}/sync${dryRun ? '?dryRun=true' : ''}`;
68
+ const response = await apiRequest(url, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: JSON.stringify({ files }),
74
+ });
75
+ if (!response.ok) {
76
+ const error = (await response.json().catch(() => ({})));
77
+ throw new Error(error.message || `Failed to sync files: ${response.statusText}`);
78
+ }
79
+ return (await response.json());
80
+ }
81
+ export async function uploadToR2(uploadUrl, content, contentType) {
82
+ const response = await fetch(uploadUrl, {
83
+ method: "PUT",
84
+ body: content,
85
+ headers: {
86
+ "Content-Type": contentType,
87
+ },
88
+ });
89
+ if (!response.ok) {
90
+ throw new Error(`Failed to upload file: ${response.statusText}`);
91
+ }
92
+ return true;
93
+ }
94
+ export async function getSiteStatus(siteId) {
95
+ const response = await apiRequest(`/api/cli/site/${siteId}/status`);
96
+ if (!response.ok) {
97
+ const error = (await response.json().catch(() => ({})));
98
+ throw new Error(error.message || `Failed to get site status: ${response.statusText}`);
99
+ }
100
+ return (await response.json());
101
+ }
102
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../lib/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ interface TokenData {
2
+ token: string;
3
+ username: string;
4
+ savedAt: string;
5
+ }
6
+ interface UserInfo {
7
+ username?: string;
8
+ email?: string;
9
+ id?: string;
10
+ }
11
+ export declare function saveToken(token: string, username: string): void;
12
+ export declare function getToken(): TokenData | null;
13
+ export declare function removeToken(): Promise<void>;
14
+ export declare function getAuthHeaders(): Record<string, string> | null;
15
+ export declare function pollForToken(apiUrl: string, deviceCode: string, interval: number, expiresIn: number): Promise<string>;
16
+ export declare function requireAuth(): Promise<UserInfo>;
17
+ export declare function getUserInfo(token: string): Promise<UserInfo>;
18
+ export {};
19
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../lib/auth.ts"],"names":[],"mappings":"AASA,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,QAAQ;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAaD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAY/D;AAMD,wBAAgB,QAAQ,IAAI,SAAS,GAAG,IAAI,CAY3C;AAKD,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAYjD;AAMD,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAU9D;AAUD,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CA8DjB;AAMD,wBAAsB,WAAW,IAAI,OAAO,CAAC,QAAQ,CAAC,CAcrD;AAOD,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYlE"}