@papercraneai/cli 1.5.6 → 1.6.0-beta.1

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 (74) hide show
  1. package/bin/papercrane.js +170 -251
  2. package/components/command-listener.tsx +52 -0
  3. package/components/dashboard-grid.tsx +61 -0
  4. package/components/error-reporter.tsx +112 -0
  5. package/components/theme-provider.tsx +10 -0
  6. package/components/theme-switcher.tsx +54 -0
  7. package/components/theme-toggle.tsx +21 -0
  8. package/components/ui/accordion.tsx +66 -0
  9. package/components/ui/alert-dialog.tsx +157 -0
  10. package/components/ui/alert.tsx +66 -0
  11. package/components/ui/aspect-ratio.tsx +11 -0
  12. package/components/ui/avatar.tsx +53 -0
  13. package/components/ui/badge.tsx +46 -0
  14. package/components/ui/breadcrumb.tsx +109 -0
  15. package/components/ui/button-group.tsx +83 -0
  16. package/components/ui/button.tsx +60 -0
  17. package/components/ui/calendar.tsx +216 -0
  18. package/components/ui/card.tsx +92 -0
  19. package/components/ui/carousel.tsx +241 -0
  20. package/components/ui/chart.tsx +357 -0
  21. package/components/ui/checkbox.tsx +32 -0
  22. package/components/ui/collapsible.tsx +33 -0
  23. package/components/ui/command.tsx +184 -0
  24. package/components/ui/context-menu.tsx +252 -0
  25. package/components/ui/dialog.tsx +143 -0
  26. package/components/ui/drawer.tsx +135 -0
  27. package/components/ui/dropdown-menu.tsx +257 -0
  28. package/components/ui/empty.tsx +104 -0
  29. package/components/ui/field.tsx +248 -0
  30. package/components/ui/form.tsx +167 -0
  31. package/components/ui/hover-card.tsx +44 -0
  32. package/components/ui/input-group.tsx +170 -0
  33. package/components/ui/input-otp.tsx +77 -0
  34. package/components/ui/input.tsx +21 -0
  35. package/components/ui/item.tsx +193 -0
  36. package/components/ui/kbd.tsx +28 -0
  37. package/components/ui/label.tsx +24 -0
  38. package/components/ui/menubar.tsx +276 -0
  39. package/components/ui/navigation-menu.tsx +168 -0
  40. package/components/ui/pagination.tsx +127 -0
  41. package/components/ui/popover.tsx +48 -0
  42. package/components/ui/progress.tsx +31 -0
  43. package/components/ui/radio-group.tsx +45 -0
  44. package/components/ui/resizable.tsx +56 -0
  45. package/components/ui/scroll-area.tsx +58 -0
  46. package/components/ui/select.tsx +187 -0
  47. package/components/ui/separator.tsx +28 -0
  48. package/components/ui/sheet.tsx +139 -0
  49. package/components/ui/sidebar.tsx +726 -0
  50. package/components/ui/skeleton.tsx +13 -0
  51. package/components/ui/slider.tsx +63 -0
  52. package/components/ui/sonner.tsx +40 -0
  53. package/components/ui/spinner.tsx +16 -0
  54. package/components/ui/switch.tsx +31 -0
  55. package/components/ui/table.tsx +116 -0
  56. package/components/ui/tabs.tsx +66 -0
  57. package/components/ui/textarea.tsx +18 -0
  58. package/components/ui/toggle-group.tsx +83 -0
  59. package/components/ui/toggle.tsx +47 -0
  60. package/components/ui/tooltip.tsx +61 -0
  61. package/lib/dev-server.js +395 -0
  62. package/lib/environment-client.js +50 -12
  63. package/package.json +65 -4
  64. package/public/themes/blue.css +69 -0
  65. package/public/themes/default.css +70 -0
  66. package/public/themes/green.css +69 -0
  67. package/public/themes/orange.css +69 -0
  68. package/public/themes/red.css +69 -0
  69. package/public/themes/rose.css +69 -0
  70. package/public/themes/violet.css +69 -0
  71. package/public/themes/yellow.css +69 -0
  72. package/runtime-hooks/use-mobile.ts +19 -0
  73. package/runtime-lib/papercrane.ts +49 -0
  74. package/runtime-lib/utils.ts +6 -0
package/bin/papercrane.js CHANGED
@@ -4,12 +4,81 @@ import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
5
  import readline from 'readline';
6
6
  import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import os from 'os';
7
9
  import http from 'http';
8
10
  import { http as httpClient } from '../lib/axios-client.js';
9
11
  import { setApiKey, clearConfig, isLoggedIn, setDefaultWorkspace, getDefaultWorkspace, setOrgId, getOrgId } from '../lib/config.js';
10
12
  import { validateApiKey } from '../lib/cloud-client.js';
11
13
  import { listFunctions, getFunction, runFunction, formatDescribe, formatDescribeRoot, formatFlat, formatResult, formatUnconnected } from '../lib/function-client.js';
12
- import { listWorkspaces, resolveWorkspaceId, getFileTree, readFile, writeFile, editFile, deleteFile, getLocalWorkspacePath, pullWorkspace, pushWorkspace } from '../lib/environment-client.js';
14
+ import { listWorkspaces, resolveWorkspaceId, getLocalWorkspacePath, pullWorkspace, pushWorkspace } from '../lib/environment-client.js';
15
+
16
+ /**
17
+ * Check login and workspace setup. Exits with helpful message if not ready.
18
+ */
19
+ async function requireWorkspace() {
20
+ const loggedIn = await isLoggedIn();
21
+ if (!loggedIn) {
22
+ console.error(chalk.red('Not logged in. Run: papercrane login'));
23
+ process.exit(1);
24
+ }
25
+ const defaultWs = await getDefaultWorkspace();
26
+ if (!defaultWs) {
27
+ console.error(chalk.red('No workspace selected.'));
28
+ console.error(chalk.dim('\nRun these commands first:'));
29
+ console.error(chalk.dim(' papercrane workspaces list'));
30
+ console.error(chalk.dim(' papercrane workspaces use <id>'));
31
+ process.exit(1);
32
+ }
33
+ return defaultWs;
34
+ }
35
+
36
+ /**
37
+ * After login, list workspaces and prompt user to select one.
38
+ */
39
+ async function showPostLoginHelp() {
40
+ try {
41
+ const workspaces = await listWorkspaces();
42
+ if (!workspaces || workspaces.length === 0) {
43
+ console.log(chalk.dim('No workspaces found. Create one at https://papercrane.ai\n'));
44
+ return;
45
+ }
46
+
47
+ if (workspaces.length === 1) {
48
+ // Auto-select the only workspace
49
+ const ws = workspaces[0];
50
+ await setDefaultWorkspace(ws.id);
51
+
52
+ const workspaceDir = getLocalWorkspacePath(ws.id);
53
+ const { generateScaffolding } = await import('../lib/dev-server.js');
54
+ await generateScaffolding(workspaceDir);
55
+
56
+ const currentLink = path.join(os.homedir(), '.papercrane', 'current');
57
+ try {
58
+ const existing = await fs.readlink(currentLink);
59
+ if (existing !== workspaceDir) {
60
+ await fs.unlink(currentLink);
61
+ await fs.symlink(workspaceDir, currentLink);
62
+ }
63
+ } catch {
64
+ try { await fs.unlink(currentLink); } catch {}
65
+ await fs.symlink(workspaceDir, currentLink);
66
+ }
67
+
68
+ console.log(chalk.green(`\n✓ Workspace set to ${ws.id}${ws.name ? ` (${ws.name})` : ''}`));
69
+ console.log(chalk.dim(` Dashboards: ${workspaceDir}/app/\n`));
70
+ return;
71
+ }
72
+
73
+ console.log(chalk.bold('\nAvailable workspaces:\n'));
74
+ for (const ws of workspaces) {
75
+ console.log(` ${chalk.bold(ws.id)} ${ws.name || '(unnamed)'}`);
76
+ }
77
+ console.log(chalk.dim('\nNext step: papercrane workspaces use <id>\n'));
78
+ } catch {
79
+ // Non-fatal — workspace listing might fail
80
+ }
81
+ }
13
82
 
14
83
  /**
15
84
  * Test if browser can actually open URLs by starting a local server and
@@ -239,7 +308,8 @@ program
239
308
  } catch {
240
309
  // Non-fatal — org ID is a convenience, not required
241
310
  }
242
- console.log(chalk.green('✓ Successfully logged in\n'));
311
+ console.log(chalk.green('✓ Successfully logged in'));
312
+ await showPostLoginHelp();
243
313
  } else {
244
314
  console.log(chalk.yellow('⚠️ API key saved, but validation failed.'));
245
315
  console.log(chalk.yellow(' The key may be invalid or the service may be unavailable.\n'));
@@ -267,7 +337,8 @@ program
267
337
  if (data.orgId) await setOrgId(data.orgId);
268
338
  await clearPendingSession();
269
339
  console.log(chalk.green(`✓ Logged in as ${data.email}`));
270
- console.log(chalk.dim(` API key saved to ~/.papercrane/config.json\n`));
340
+ console.log(chalk.dim(` API key saved to ~/.papercrane/config.json`));
341
+ await showPostLoginHelp();
271
342
  return;
272
343
  }
273
344
  }
@@ -284,7 +355,8 @@ program
284
355
  if (result.orgId) await setOrgId(result.orgId);
285
356
 
286
357
  console.log(chalk.green(`✓ Logged in as ${result.email}`));
287
- console.log(chalk.dim(` API key saved to ~/.papercrane/config.json\n`));
358
+ console.log(chalk.dim(` API key saved to ~/.papercrane/config.json`));
359
+ await showPostLoginHelp();
288
360
  } catch (error) {
289
361
  if (error.message.includes('timed out')) {
290
362
  console.error(chalk.red('\nAuthentication timed out. Please try again.'));
@@ -386,7 +458,7 @@ program
386
458
 
387
459
  program
388
460
  .command('add [integration]')
389
- .description('Add a new integration via OAuth')
461
+ .description('Connect a new integration')
390
462
  .action(async (integration) => {
391
463
  try {
392
464
  if (integration) {
@@ -458,19 +530,22 @@ program
458
530
 
459
531
  program
460
532
  .command('dashboard-guide')
461
- .description('Show how to build dashboards in cloud workspaces')
533
+ .description('Show how to build dashboards')
462
534
  .action(async () => {
463
- console.log(chalk.bold('\n📊 Building Dashboards in Papercrane Workspaces\n'));
464
- console.log(chalk.dim('Workspaces are Next.js environments. Files you write become live routes.\n'));
535
+ console.log(chalk.bold('\n📊 Building Dashboards with Papercrane\n'));
536
+ console.log(chalk.dim('Each dashboard is a folder with two files: page.tsx (UI) and action.ts (data).\n'));
465
537
 
466
538
  console.log(chalk.bold('FILE STRUCTURE'));
467
539
  console.log(chalk.dim('─'.repeat(60)));
468
540
  console.log(`
469
- Create two files for each dashboard page:
470
-
471
- app/(dashboard)/my-dashboard/
472
- ├── action.ts # Server-side data fetching
473
- └── page.tsx # Client component with charts
541
+ ~/.papercrane/current/app/
542
+ ├── layout.tsx # Auto-generated by CLI
543
+ ├── globals.css # Auto-generated by CLI
544
+ └── my-dashboard/ # Your dashboard
545
+ ├── action.ts # Server-side data fetching
546
+ └── page.tsx # Client component with charts
547
+
548
+ Set active workspace: papercrane workspaces use <id>
474
549
  `);
475
550
 
476
551
  console.log(chalk.bold('1. action.ts - Server Action Pattern'));
@@ -577,15 +652,18 @@ Use these Tailwind classes for theme-aware colors:
577
652
  console.log(chalk.bold('QUICK START'));
578
653
  console.log(chalk.dim('─'.repeat(60)));
579
654
  console.log(`
580
- 1. ${chalk.cyan('papercrane')} # List available integrations
581
- 2. ${chalk.cyan('papercrane describe <path>')} # Explore endpoints
582
- 3. ${chalk.cyan('papercrane call <path> \'{}\'')} # Test data fetching
655
+ 1. ${chalk.cyan('papercrane login')} # Authenticate with Papercrane
656
+ 2. ${chalk.cyan('papercrane workspaces list')} # See available workspaces
657
+ 3. ${chalk.cyan('papercrane workspaces use <id>')} # Set active workspace
583
658
  4. ${chalk.cyan('papercrane pull')} # Pull workspace files locally
584
- 5. Edit files in ${chalk.cyan('~/.papercrane/workspaces/<id>/')}
585
- 6. ${chalk.cyan('papercrane push')} # Push changes back
586
-
587
- ${chalk.dim('The pull/push workflow is recommended - edit files locally, then sync.')}
588
- ${chalk.dim('For single files, you can also use: papercrane files write <path> --json "..."')}
659
+ 5. ${chalk.cyan('papercrane dev')} # Start local dev server with HMR
660
+ 6. Create dashboards in ${chalk.cyan('~/.papercrane/current/app/')}
661
+ 7. ${chalk.cyan('papercrane push')} # Push to cloud, get shareable URLs
662
+
663
+ ${chalk.dim('Explore APIs:')}
664
+ ${chalk.cyan('papercrane')} # List connected integrations
665
+ ${chalk.cyan('papercrane describe <path>')} # Explore endpoints
666
+ ${chalk.cyan('papercrane call <path> \'{}\'')} # Test data fetching
589
667
  `);
590
668
  console.log();
591
669
  });
@@ -634,7 +712,8 @@ workspacesCmd
634
712
  workspacesCmd
635
713
  .command('use <id>')
636
714
  .description('Set the default workspace')
637
- .action(async (id) => {
715
+ .option('--reset-scaffolding', 'Regenerate scaffolding files (layout.tsx, globals.css, configs)')
716
+ .action(async (id, options) => {
638
717
  try {
639
718
  const loggedIn = await isLoggedIn();
640
719
  if (!loggedIn) {
@@ -649,206 +728,78 @@ workspacesCmd
649
728
  }
650
729
 
651
730
  await setDefaultWorkspace(wsId);
652
- console.log(chalk.green(`✓ Default workspace set to ${wsId}\n`));
653
- } catch (error) {
654
- console.error(chalk.red('Error:'), error.message);
655
- process.exit(1);
656
- }
657
- });
658
-
659
- // ============================================================================
660
- // FILE COMMANDS
661
- // ============================================================================
662
731
 
663
- const filesCmd = program.command('files').description('Build dashboards by reading/writing files in cloud workspaces.\n\nWorkspaces are Next.js environments - files you write become live routes.\nRun "papercrane dashboard-guide" for the recommended dashboard pattern.');
732
+ // Scaffold the workspace and create ~/.papercrane/current symlink
733
+ const { generateScaffolding, resetScaffolding } = await import('../lib/dev-server.js');
734
+ const workspaceDir = getLocalWorkspacePath(wsId);
664
735
 
665
- filesCmd
666
- .command('list [path]')
667
- .description('List files in a workspace')
668
- .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
669
- .action(async (path, options) => {
670
- try {
671
- const loggedIn = await isLoggedIn();
672
- if (!loggedIn) {
673
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
674
- process.exit(1);
736
+ if (options.resetScaffolding) {
737
+ await resetScaffolding(workspaceDir);
738
+ await fs.rm(path.join(workspaceDir, '.next'), { recursive: true, force: true });
739
+ console.log(chalk.dim('Scaffolding regenerated.'));
675
740
  }
741
+ await generateScaffolding(workspaceDir);
676
742
 
677
- const wsId = await resolveWorkspaceId(options.workspace);
678
- const result = await getFileTree(wsId, path || '');
679
-
680
- console.log(chalk.bold(`\nFiles in workspace ${wsId}${path ? ` (${path})` : ''}:\n`));
681
- printFileTree(result.tree, ' ');
682
- console.log();
683
- } catch (error) {
684
- console.error(chalk.red('Error:'), error.message);
685
- process.exit(1);
686
- }
687
- });
688
-
689
- filesCmd
690
- .command('read <path>')
691
- .description('Read file contents')
692
- .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
693
- .option('--raw', 'Output raw content without formatting')
694
- .action(async (path, options) => {
695
- try {
696
- const loggedIn = await isLoggedIn();
697
- if (!loggedIn) {
698
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
699
- process.exit(1);
743
+ const currentLink = path.join(os.homedir(), '.papercrane', 'current');
744
+ try {
745
+ const existing = await fs.readlink(currentLink);
746
+ if (existing !== workspaceDir) {
747
+ await fs.unlink(currentLink);
748
+ await fs.symlink(workspaceDir, currentLink);
749
+ }
750
+ } catch {
751
+ try { await fs.unlink(currentLink); } catch {}
752
+ await fs.symlink(workspaceDir, currentLink);
700
753
  }
701
754
 
702
- const wsId = await resolveWorkspaceId(options.workspace);
703
- const result = await readFile(wsId, path);
704
-
705
- if (options.raw) {
706
- process.stdout.write(result.content);
707
- } else {
708
- console.log(chalk.dim(`# ${result.path} (${result.size} bytes) — use this path for write/edit/delete\n`));
709
- console.log(result.content);
710
- }
755
+ console.log(chalk.green(`✓ Default workspace set to ${wsId}`));
756
+ console.log(chalk.dim(` ${currentLink} ${workspaceDir}`));
757
+ console.log(chalk.dim(` Dashboards: ${workspaceDir}/app/\n`));
711
758
  } catch (error) {
712
759
  console.error(chalk.red('Error:'), error.message);
713
760
  process.exit(1);
714
761
  }
715
762
  });
716
763
 
717
- filesCmd
718
- .command('write <path> [content]')
719
- .description('Write content to a file. Path is relative to workspace root (same paths as files list/read).\n\nExamples:\n papercrane files write app/page.tsx --json \'"use client";\\nexport default function() {}"\'\n papercrane files write app/action.ts -f ./local-file.ts')
720
- .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
721
- .option('-f, --file <localPath>', 'Read content from a local file')
722
- .option('--json <string>', 'Content as a JSON-encoded string (recommended for code)')
723
- .option('--stdin', 'Read content from stdin')
724
- .action(async (path, contentArg, options) => {
725
- try {
726
- const loggedIn = await isLoggedIn();
727
- if (!loggedIn) {
728
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
729
- process.exit(1);
730
- }
731
-
732
- let content;
733
-
734
- if (options.json) {
735
- // Parse JSON-encoded string
736
- try {
737
- content = JSON.parse(options.json);
738
- if (typeof content !== 'string') {
739
- console.error(chalk.red('Error: --json must be a JSON string, e.g., \'"hello\\nworld"\''));
740
- process.exit(1);
741
- }
742
- } catch (e) {
743
- console.error(chalk.red('Error: Invalid JSON string'));
744
- console.error(chalk.dim(' Expected format: \'"line1\\nline2"\''));
745
- process.exit(1);
746
- }
747
- } else if (options.file) {
748
- // Read from local file
749
- content = await fs.readFile(options.file, 'utf-8');
750
- } else if (options.stdin) {
751
- // Read from stdin
752
- content = await readStdin();
753
- } else if (contentArg) {
754
- // Use positional argument
755
- content = contentArg;
756
- } else {
757
- console.error(chalk.red('Error: Content required. Provide --json, --file, --stdin, or as argument'));
758
- process.exit(1);
759
- }
760
-
761
- const wsId = await resolveWorkspaceId(options.workspace);
762
- const result = await writeFile(wsId, path, content);
763
764
 
764
- console.log(chalk.green(`✓ Written ${result.size} bytes to ${result.path}\n`));
765
- } catch (error) {
766
- if (error.response?.status === 404) {
767
- console.error(chalk.red('Error: 404 — workspace sandbox may be stopped or unreachable'));
768
- console.error(chalk.dim(` Path: ${path}`));
769
- console.error(chalk.dim(' Check workspace status: papercrane workspaces list'));
770
- } else {
771
- console.error(chalk.red('Error:'), error.response?.data?.error || error.message);
772
- }
773
- process.exit(1);
774
- }
775
- });
765
+ // ============================================================================
766
+ // DEV COMMAND
767
+ // ============================================================================
776
768
 
777
- filesCmd
778
- .command('edit <path>')
779
- .description('Edit a file with find/replace')
780
- .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
781
- .option('--old <string>', 'Text to find')
782
- .option('--new <string>', 'Text to replace with')
783
- .option('--replace-all', 'Replace all occurrences')
784
- .option('-p, --params <json>', 'Parameters as JSON: {"old_string": "...", "new_string": "...", "replace_all": false}')
785
- .option('--stdin', 'Read parameters as JSON from stdin')
786
- .action(async (path, options) => {
769
+ program
770
+ .command('dev')
771
+ .description('Start local dev server for dashboard development with HMR')
772
+ .option('-w, --workspace <id>', 'Workspace ID')
773
+ .option('-p, --port <port>', 'Port (default: 3100)', '3100')
774
+ .option('--clear-cache', 'Clear build cache before starting (use after structural changes like editing globals.css or adding dependencies)')
775
+ .option('--reset-scaffolding', 'Regenerate scaffolding files (layout.tsx, globals.css, configs) and clear build cache')
776
+ .action(async (options) => {
787
777
  try {
788
- const loggedIn = await isLoggedIn();
789
- if (!loggedIn) {
790
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
791
- process.exit(1);
792
- }
793
-
794
- let oldString, newString, replaceAll = false;
795
-
796
- if (options.stdin) {
797
- // Read JSON from stdin
798
- const input = await readStdin();
799
- const params = JSON.parse(input);
800
- oldString = params.old_string;
801
- newString = params.new_string;
802
- replaceAll = params.replace_all || false;
803
- } else if (options.params) {
804
- // Parse JSON params
805
- const params = JSON.parse(options.params);
806
- oldString = params.old_string;
807
- newString = params.new_string;
808
- replaceAll = params.replace_all || false;
809
- } else if (options.old !== undefined && options.new !== undefined) {
810
- // Use --old and --new flags
811
- oldString = options.old;
812
- newString = options.new;
813
- replaceAll = options.replaceAll || false;
814
- } else {
815
- console.error(chalk.red('Error: Must provide --old and --new, or --params, or --stdin'));
816
- process.exit(1);
817
- }
778
+ const { generateScaffolding, resetScaffolding, startDevServer } = await import('../lib/dev-server.js');
818
779
 
819
- const wsId = await resolveWorkspaceId(options.workspace);
820
- const result = await editFile(wsId, path, oldString, newString, replaceAll);
780
+ const wsId = options.workspace ? parseInt(options.workspace, 10) : await requireWorkspace();
781
+ const workspaceDir = getLocalWorkspacePath(wsId);
821
782
 
822
- console.log(chalk.green(`✓ Made ${result.replacements} replacement(s) in ${result.path}\n`));
823
- } catch (error) {
824
- if (error.response?.data?.error) {
825
- console.error(chalk.red('Error:'), error.response.data.error);
826
- if (error.response.data.occurrences) {
827
- console.error(chalk.dim(` Found ${error.response.data.occurrences} occurrences. Use --replace-all to replace all.`));
828
- }
829
- } else {
830
- console.error(chalk.red('Error:'), error.message);
783
+ if (options.resetScaffolding) {
784
+ await resetScaffolding(workspaceDir);
785
+ console.log(chalk.dim('Scaffolding regenerated.'));
831
786
  }
832
- process.exit(1);
833
- }
834
- });
835
787
 
836
- filesCmd
837
- .command('delete <path>')
838
- .description('Delete a file')
839
- .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
840
- .action(async (path, options) => {
841
- try {
842
- const loggedIn = await isLoggedIn();
843
- if (!loggedIn) {
844
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
845
- process.exit(1);
788
+ if (options.clearCache || options.resetScaffolding) {
789
+ const nextDir = path.join(workspaceDir, '.next');
790
+ await fs.rm(nextDir, { recursive: true, force: true });
791
+ console.log(chalk.dim('Build cache cleared.'));
846
792
  }
847
793
 
848
- const wsId = await resolveWorkspaceId(options.workspace);
849
- const result = await deleteFile(wsId, path);
794
+ await generateScaffolding(workspaceDir);
795
+
796
+ const port = parseInt(options.port, 10);
797
+ const dashboardDir = workspaceDir + '/app';
798
+ console.log(chalk.green(`\nDev server starting on http://localhost:${port}`));
799
+ console.log(chalk.dim(`Dashboard dir: ${dashboardDir}`));
800
+ console.log(chalk.dim('Create dashboard folders with page.tsx + action.ts\n'));
850
801
 
851
- console.log(chalk.green(`✓ Deleted ${result.path}\n`));
802
+ await startDevServer(workspaceDir, port);
852
803
  } catch (error) {
853
804
  console.error(chalk.red('Error:'), error.message);
854
805
  process.exit(1);
@@ -865,13 +816,7 @@ program
865
816
  .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
866
817
  .action(async (options) => {
867
818
  try {
868
- const loggedIn = await isLoggedIn();
869
- if (!loggedIn) {
870
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
871
- process.exit(1);
872
- }
873
-
874
- const wsId = await resolveWorkspaceId(options.workspace);
819
+ const wsId = options.workspace ? parseInt(options.workspace, 10) : await requireWorkspace();
875
820
 
876
821
  console.log(chalk.cyan(`Pulling workspace ${wsId}...`));
877
822
 
@@ -879,9 +824,15 @@ program
879
824
  process.stdout.write(`\r ${chalk.dim(`[${current}/${total}]`)} ${file}`);
880
825
  });
881
826
 
827
+ // Set up dev server infrastructure after pull
828
+ const { generateScaffolding } = await import('../lib/dev-server.js');
829
+ await generateScaffolding(localPath);
830
+
882
831
  console.log('\n');
883
832
  console.log(chalk.green(`✓ Pulled ${files.length} files to ${localPath}\n`));
884
- console.log(chalk.dim('Edit files locally, then run: papercrane push'));
833
+ console.log(chalk.dim('Dashboards are in: ' + localPath + '/app/'));
834
+ console.log(chalk.dim('Start local dev server: papercrane dev'));
835
+ console.log(chalk.dim('Push changes back: papercrane push'));
885
836
  } catch (error) {
886
837
  console.error(chalk.red('Error:'), error.message);
887
838
  process.exit(1);
@@ -890,18 +841,12 @@ program
890
841
 
891
842
  program
892
843
  .command('push')
893
- .description('Push local files back to workspace')
844
+ .description('Push dashboards and resources to cloud for sharing')
894
845
  .option('-w, --workspace <id>', 'Workspace ID (uses default if not specified)')
895
- .option('-p, --path <path>', 'Only push files under this path (e.g., "verdata-vrisk")')
846
+ .option('-p, --path <path>', 'Only push files under this path (e.g., "ga4-dashboard")')
896
847
  .action(async (options) => {
897
848
  try {
898
- const loggedIn = await isLoggedIn();
899
- if (!loggedIn) {
900
- console.error(chalk.red('Error: Not logged in. Run: papercrane login'));
901
- process.exit(1);
902
- }
903
-
904
- const wsId = await resolveWorkspaceId(options.workspace);
849
+ const wsId = options.workspace ? parseInt(options.workspace, 10) : await requireWorkspace();
905
850
  const localPath = getLocalWorkspacePath(wsId);
906
851
  const pathFilter = options.path;
907
852
 
@@ -912,19 +857,20 @@ program
912
857
  }, pathFilter);
913
858
 
914
859
  console.log('\n');
915
- console.log(chalk.green(`✓ Pushed ${files.length} files from ${localPath}`));
860
+ console.log(chalk.green(`✓ Pushed ${files.length} files to cloud`));
916
861
 
917
- // Print preview URL(s)
862
+ // Print shareable URLs
918
863
  const { getApiBaseUrl, getOrgId: getStoredOrgId } = await import('../lib/config.js');
919
864
  const baseUrl = await getApiBaseUrl();
920
865
  const orgId = await getStoredOrgId();
921
866
 
922
- // Find unique dashboard directories that were pushed
923
- const dashboards = [...new Set(files.map(f => f.split('/')[0]).filter(d => d && !d.includes('.')))];
867
+ const dashboards = [...new Set(files
868
+ .filter(f => f.startsWith('app/'))
869
+ .map(f => f.split('/')[1])
870
+ .filter(d => d && !d.includes('.')))];
924
871
 
925
872
  if (dashboards.length > 0) {
926
873
  console.log('');
927
- console.log(chalk.dim('Preview:'));
928
874
  const pathPrefix = orgId ? `/org/${orgId}` : '';
929
875
  for (const dashboard of dashboards) {
930
876
  console.log(chalk.cyan(` ${baseUrl}${pathPrefix}/environments/${wsId}/${dashboard}`));
@@ -948,29 +894,6 @@ function readStdin() {
948
894
  });
949
895
  }
950
896
 
951
- // Helper to print file tree recursively
952
- function printFileTree(tree, indent = '') {
953
- if (!tree || !Array.isArray(tree)) return;
954
-
955
- // Sort: folders first, then files
956
- const sorted = [...tree].sort((a, b) => {
957
- if (a.type === 'folder' && b.type !== 'folder') return -1;
958
- if (a.type !== 'folder' && b.type === 'folder') return 1;
959
- return a.name.localeCompare(b.name);
960
- });
961
-
962
- for (const item of sorted) {
963
- if (item.type === 'folder') {
964
- console.log(`${indent}${chalk.blue(item.name + '/')}`);
965
- if (item.children) {
966
- printFileTree(item.children, indent + ' ');
967
- }
968
- } else {
969
- console.log(`${indent}${item.name}`);
970
- }
971
- }
972
- }
973
-
974
897
  // Default action: show connected modules when no command is given
975
898
  program.action(async (_, cmd) => {
976
899
  // Reject unknown commands instead of silently falling through
@@ -986,13 +909,9 @@ program.action(async (_, cmd) => {
986
909
  console.error(' papercrane dashboard-guide Show how to build dashboards');
987
910
  console.error(' papercrane workspaces list List workspaces');
988
911
  console.error(' papercrane workspaces use <id> Set default workspace');
989
- console.error(' papercrane files list [path] List files in workspace');
990
- console.error(' papercrane files read <path> Read file contents');
991
- console.error(' papercrane files write <path> Write file contents');
992
- console.error(' papercrane files edit <path> Edit file with find/replace');
993
- console.error(' papercrane files delete <path> Delete a file');
912
+ console.error(' papercrane dev Start local dev server with HMR');
994
913
  console.error(' papercrane pull Pull workspace files locally for editing');
995
- console.error(' papercrane push Push local changes back to workspace');
914
+ console.error(' papercrane push Push to cloud, get shareable URLs');
996
915
  console.error(' papercrane login Log in with API key');
997
916
  console.error(' papercrane logout Log out');
998
917
  process.exit(1);
@@ -0,0 +1,52 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef } from "react"
4
+
5
+ interface CommandHandler {
6
+ handleCommand: (command: { action: string; value?: string }) => void
7
+ }
8
+
9
+ export function CommandListener() {
10
+ const handlerRef = useRef<CommandHandler | null>(null)
11
+
12
+ // Try to load the plugin and set up the command handler
13
+ useEffect(() => {
14
+ let cancelled = false
15
+
16
+ async function init() {
17
+ try {
18
+ const pkg = "@papercrane/" + "dashboard-grid/plugin"
19
+ const mod = await import(/* turbopackIgnore: true */ pkg)
20
+ if (!cancelled && mod.createCommandHandler) {
21
+ handlerRef.current = mod.createCommandHandler()
22
+ }
23
+ } catch {
24
+ // Plugin not installed — commands are silently ignored
25
+ }
26
+ }
27
+
28
+ init()
29
+ return () => { cancelled = true }
30
+ }, [])
31
+
32
+ useEffect(() => {
33
+ // Only listen when embedded in an iframe
34
+ if (typeof window === "undefined" || window === window.parent) {
35
+ return
36
+ }
37
+
38
+ const handleMessage = (event: MessageEvent) => {
39
+ const data = event.data
40
+ if (!data || typeof data !== "object" || data.type !== "papercrane-command") {
41
+ return
42
+ }
43
+
44
+ handlerRef.current?.handleCommand(data.command)
45
+ }
46
+
47
+ window.addEventListener("message", handleMessage)
48
+ return () => window.removeEventListener("message", handleMessage)
49
+ }, [])
50
+
51
+ return null
52
+ }