diff --git a/.changeset/pre.json b/.changeset/pre.json index 925bc9e3..9b57e8d9 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,7 +2,6 @@ "mode": "pre", "tag": "beta", "initialVersions": { - "@pulse-editor/capacitor-plugin": "0.0.1", "@pulse-editor/mobile": "0.0.1", "@pulse-editor/react-api": "0.1.1-alpha.54", "@pulse-editor/shared-utils": "0.1.1-alpha.54", diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index caa7ec77..3b842587 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -32,13 +32,7 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - + - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 46cd344d..43dc14a0 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -42,7 +42,7 @@ jobs: - name: Get Keystore run: | mkdir ~/.keystore - echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/keystore.jks + echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/pulse-editor.keystore - name: Setup Node.js uses: actions/setup-node@v4 @@ -55,12 +55,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - - name: Install web dependencies run: npm install --workspace=web @@ -78,5 +72,5 @@ jobs: working-directory: mobile - name: Build Capacitor App - run: npx cap build android --keystorepath ~/.keystore/keystore.jks --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK + run: npx cap build android --keystorepath ~/.keystore/pulse-editor.keystore --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK working-directory: mobile diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index ce6f5486..d53700ec 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -28,12 +28,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index dd78a626..a5800311 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -30,12 +30,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 00823cf4..7590004e 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -39,7 +39,7 @@ jobs: - name: Get Keystore run: | mkdir ~/.keystore - echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/keystore.jks + echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/pulse-editor.keystore - name: Setup Node.js uses: actions/setup-node@v4 @@ -52,12 +52,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - - name: Install web dependencies run: npm install --workspace=web @@ -75,7 +69,7 @@ jobs: working-directory: mobile - name: Build Capacitor App - run: npx cap build android --keystorepath ~/.keystore/keystore.jks --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK + run: npx cap build android --keystorepath ~/.keystore/pulse-editor.keystore --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK working-directory: mobile - name: Move APK diff --git a/capacitor-plugin/.eslintignore b/capacitor-plugin/.eslintignore deleted file mode 100644 index 23f89447..00000000 --- a/capacitor-plugin/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -build -dist -example-app diff --git a/capacitor-plugin/.gitignore b/capacitor-plugin/.gitignore deleted file mode 100644 index df9f0c20..00000000 --- a/capacitor-plugin/.gitignore +++ /dev/null @@ -1,70 +0,0 @@ -# node files -dist -node_modules - -# iOS files -Pods -Podfile.lock -Package.resolved -Build -xcuserdata -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc - - -# macOS files -.DS_Store - - - -# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore - -# Built application files -*.apk -*.ap_ - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin -gen -out - -# Gradle files -.gradle -build - -# Local configuration file (sdk path, etc) -local.properties - -# Proguard folder generated by Eclipse -proguard - -# Log Files -*.log - -# Android Studio Navigation editor temp files -.navigation - -# Android Studio captures folder -captures - -# IntelliJ -*.iml -.idea - -# Keystore files -# Uncomment the following line if you do not want to check your keystore files in. -#*.jks - -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild diff --git a/capacitor-plugin/.prettierignore b/capacitor-plugin/.prettierignore deleted file mode 100644 index 5ab6e14d..00000000 --- a/capacitor-plugin/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -example-app diff --git a/capacitor-plugin/CONTRIBUTING.md b/capacitor-plugin/CONTRIBUTING.md deleted file mode 100644 index 3f875180..00000000 --- a/capacitor-plugin/CONTRIBUTING.md +++ /dev/null @@ -1,52 +0,0 @@ -# Contributing - -This guide provides instructions for contributing to this Capacitor plugin. - -## Developing - -### Local Setup - -1. Fork and clone the repo. -1. Install the dependencies. - - ```shell - npm install - ``` - -1. Install SwiftLint if you're on macOS. - - ```shell - brew install swiftlint - ``` - -### Scripts - -#### `npm run build` - -Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen). - -It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported. - -Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`. - -#### `npm run verify` - -Build and validate the web and native projects. - -This is useful to run in CI to verify that the plugin builds for all platforms. - -#### `npm run lint` / `npm run fmt` - -Check formatting and code quality, autoformat/autofix if possible. - -This template is integrated with ESLint, Prettier, and SwiftLint. Using these tools is completely optional, but the [Capacitor Community](https://github.com/capacitor-community/) strives to have consistent code style and structure for easier cooperation. - -## Publishing - -There is a `prepublishOnly` hook in `package.json` which prepares the plugin before publishing, so all you need to do is run: - -```shell -npm publish -``` - -> **Note**: The [`files`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files) array in `package.json` specifies which files get published. If you rename files/directories or add files elsewhere, you may need to update it. diff --git a/capacitor-plugin/Package.swift b/capacitor-plugin/Package.swift deleted file mode 100644 index cabd9ee1..00000000 --- a/capacitor-plugin/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "PulseEditorCapacitorPlugin", - platforms: [.iOS(.v14)], - products: [ - .library( - name: "PulseEditorCapacitorPlugin", - targets: ["PulseEditorCapacitorPlugin"]) - ], - dependencies: [ - .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0") - ], - targets: [ - .target( - name: "PulseEditorCapacitorPlugin", - dependencies: [ - .product(name: "Capacitor", package: "capacitor-swift-pm"), - .product(name: "Cordova", package: "capacitor-swift-pm") - ], - path: "ios/Sources/PulseEditorCapacitorPlugin"), - .testTarget( - name: "PulseEditorCapacitorPluginTests", - dependencies: ["PulseEditorCapacitorPlugin"], - path: "ios/Tests/PulseEditorCapacitorPluginTests") - ] -) \ No newline at end of file diff --git a/capacitor-plugin/PulseEditorCapacitorPlugin.podspec b/capacitor-plugin/PulseEditorCapacitorPlugin.podspec deleted file mode 100644 index db20bf67..00000000 --- a/capacitor-plugin/PulseEditorCapacitorPlugin.podspec +++ /dev/null @@ -1,17 +0,0 @@ -require 'json' - -package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) - -Pod::Spec.new do |s| - s.name = 'PulseEditorCapacitorPlugin' - s.version = package['version'] - s.summary = package['description'] - s.license = package['license'] - s.homepage = package['repository']['url'] - s.author = package['author'] - s.source = { :git => package['repository']['url'], :tag => s.version.to_s } - s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}' - s.ios.deployment_target = '14.0' - s.dependency 'Capacitor' - s.swift_version = '5.1' -end diff --git a/capacitor-plugin/README.md b/capacitor-plugin/README.md deleted file mode 100644 index d4c17e4d..00000000 --- a/capacitor-plugin/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# @pulse-editor/capacitor-plugin - -Capacitor plugins for Pulse Editor - -## Install - -```bash -npm install @pulse-editor/capacitor-plugin -npx cap sync -``` - -## API - - - -* [`echo(...)`](#echo) -* [`startManageStorageIntent()`](#startmanagestorageintent) -* [`isManageStoragePermissionGranted()`](#ismanagestoragepermissiongranted) - - - - - - -### echo(...) - -```typescript -echo(options: { value: string; }) => Promise<{ value: string; }> -``` - -| Param | Type | -| ------------- | ------------------------------- | -| **`options`** | { value: string; } | - -**Returns:** Promise<{ value: string; }> - --------------------- - - -### startManageStorageIntent() - -```typescript -startManageStorageIntent() => Promise -``` - --------------------- - - -### isManageStoragePermissionGranted() - -```typescript -isManageStoragePermissionGranted() => Promise<{ isGranted: boolean; }> -``` - -**Returns:** Promise<{ isGranted: boolean; }> - --------------------- - - diff --git a/capacitor-plugin/android/.gitignore b/capacitor-plugin/android/.gitignore deleted file mode 100644 index 796b96d1..00000000 --- a/capacitor-plugin/android/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/capacitor-plugin/android/build.gradle b/capacitor-plugin/android/build.gradle deleted file mode 100644 index 8bfe171f..00000000 --- a/capacitor-plugin/android/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -ext { - junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' - androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0' - androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1' - androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1' -} - -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.7.2' - } -} - -apply plugin: 'com.android.library' - -android { - namespace "com.pulse.capacitor.plugin" - compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35 - defaultConfig { - minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23 - targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - lintOptions { - abortOnError false - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_21 - targetCompatibility JavaVersion.VERSION_21 - } -} - -repositories { - google() - mavenCentral() -} - - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':capacitor-android') - implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" - testImplementation "junit:junit:$junitVersion" - androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" - androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" -} diff --git a/capacitor-plugin/android/gradle.properties b/capacitor-plugin/android/gradle.properties deleted file mode 100644 index 2e87c52f..00000000 --- a/capacitor-plugin/android/gradle.properties +++ /dev/null @@ -1,22 +0,0 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true diff --git a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.jar b/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b95..00000000 Binary files a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties b/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c1d5e018..00000000 --- a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/capacitor-plugin/android/gradlew b/capacitor-plugin/android/gradlew deleted file mode 100644 index f5feea6d..00000000 --- a/capacitor-plugin/android/gradlew +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/capacitor-plugin/android/gradlew.bat b/capacitor-plugin/android/gradlew.bat deleted file mode 100644 index 9d21a218..00000000 --- a/capacitor-plugin/android/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/capacitor-plugin/android/proguard-rules.pro b/capacitor-plugin/android/proguard-rules.pro deleted file mode 100644 index f1b42451..00000000 --- a/capacitor-plugin/android/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/capacitor-plugin/android/settings.gradle b/capacitor-plugin/android/settings.gradle deleted file mode 100644 index e558db63..00000000 --- a/capacitor-plugin/android/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') \ No newline at end of file diff --git a/capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java deleted file mode 100644 index 58020e16..00000000 --- a/capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcapacitor.android; - -import static org.junit.Assert.*; - -import android.content.Context; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("com.getcapacitor.android", appContext.getPackageName()); - } -} diff --git a/capacitor-plugin/android/src/main/AndroidManifest.xml b/capacitor-plugin/android/src/main/AndroidManifest.xml deleted file mode 100644 index a2f47b60..00000000 --- a/capacitor-plugin/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java b/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java deleted file mode 100644 index 4f018709..00000000 --- a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.pulse.capacitor.plugin; - -import android.util.Log; -import android.content.Intent; -import android.app.Activity; -import android.net.Uri; -import android.provider.Settings; -import android.os.Environment; - -public class PulseEditorCapacitor { - - public String echo(String value) { - Log.i("Echo", value); - return value; - } - - public void startManageStorageIntent(Activity activity, String packageName) { - Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - activity.startActivity(intent); - } - - public boolean isManageStoragePermissionGranted() { - return Environment.isExternalStorageManager(); - } -} diff --git a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java b/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java deleted file mode 100644 index 192caa65..00000000 --- a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.pulse.capacitor.plugin; - -import com.getcapacitor.JSObject; -import com.getcapacitor.Plugin; -import com.getcapacitor.PluginCall; -import com.getcapacitor.PluginMethod; -import com.getcapacitor.annotation.CapacitorPlugin; -import com.getcapacitor.annotation.Permission; -import android.app.Activity; - -@CapacitorPlugin( - name = "PulseEditorCapacitor", - permissions = { - @Permission( - alias = "storage", - strings = { - "Manifest.permission.MANAGE_EXTERNAL_STORAGE" - } - ) - } -) -public class PulseEditorCapacitorPlugin extends Plugin { - - private PulseEditorCapacitor implementation = new PulseEditorCapacitor(); - - @PluginMethod - public void echo(PluginCall call) { - String value = call.getString("value"); - - JSObject ret = new JSObject(); - ret.put("value", implementation.echo(value)); - call.resolve(ret); - } - - @PluginMethod - public void startManageStorageIntent(PluginCall call) { - Activity activity = getActivity(); - String packageName = activity.getPackageName(); - implementation.startManageStorageIntent(activity, packageName); - call.resolve(); - } - - @PluginMethod - public void isManageStoragePermissionGranted(PluginCall call) { - boolean isGranted = implementation.isManageStoragePermissionGranted(); - JSObject ret = new JSObject(); - ret.put("isGranted", isGranted); - call.resolve(ret); - } -} diff --git a/capacitor-plugin/android/src/main/res/.gitkeep b/capacitor-plugin/android/src/main/res/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java b/capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java deleted file mode 100644 index a0fed0cf..00000000 --- a/capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcapacitor; - -import static org.junit.Assert.*; - -import org.junit.Test; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} diff --git a/capacitor-plugin/ios/.gitignore b/capacitor-plugin/ios/.gitignore deleted file mode 100644 index afb34f83..00000000 --- a/capacitor-plugin/ios/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc \ No newline at end of file diff --git a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift b/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift deleted file mode 100644 index 8357a184..00000000 --- a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -@objc public class PulseEditorCapacitor: NSObject { - @objc public func echo(_ value: String) -> String { - print(value) - return value - } -} diff --git a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift b/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift deleted file mode 100644 index de2208a7..00000000 --- a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import Capacitor - -/** - * Please read the Capacitor iOS Plugin Development Guide - * here: https://capacitorjs.com/docs/plugins/ios - */ -@objc(PulseEditorCapacitorPlugin) -public class PulseEditorCapacitorPlugin: CAPPlugin, CAPBridgedPlugin { - public let identifier = "PulseEditorCapacitorPlugin" - public let jsName = "PulseEditorCapacitor" - public let pluginMethods: [CAPPluginMethod] = [ - CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise) - ] - private let implementation = PulseEditorCapacitor() - - @objc func echo(_ call: CAPPluginCall) { - let value = call.getString("value") ?? "" - call.resolve([ - "value": implementation.echo(value) - ]) - } -} diff --git a/capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift b/capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift deleted file mode 100644 index 4b07cc5c..00000000 --- a/capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import PulseEditorCapacitorPlugin - -class PulseEditorCapacitorTests: XCTestCase { - func testEcho() { - // This is an example of a functional test case for a plugin. - // Use XCTAssert and related functions to verify your tests produce the correct results. - - let implementation = PulseEditorCapacitor() - let value = "Hello, World!" - let result = implementation.echo(value) - - XCTAssertEqual(value, result) - } -} diff --git a/capacitor-plugin/package.json b/capacitor-plugin/package.json deleted file mode 100644 index 7406e126..00000000 --- a/capacitor-plugin/package.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "@pulse-editor/capacitor-plugin", - "version": "0.0.1", - "private": true, - "description": "Capacitor plugins for Pulse Editor", - "main": "dist/plugin.cjs.js", - "module": "dist/esm/index.js", - "types": "dist/esm/index.d.ts", - "unpkg": "dist/plugin.js", - "files": [ - "android/src/main/", - "android/build.gradle", - "dist/", - "ios/Sources", - "ios/Tests", - "Package.swift", - "PulseEditorCapacitorPlugin.podspec" - ], - "author": "ClayPulse", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/ClayPulse/pulse-editor.git" - }, - "bugs": { - "url": "https://github.com/ClayPulse/pulse-editor/issues" - }, - "keywords": [ - "capacitor", - "plugin", - "native" - ], - "scripts": { - "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", - "verify:ios": "xcodebuild -scheme PulseEditorCapacitorPlugin -destination generic/platform=iOS", - "verify:android": "cd android && gradlew clean build test && cd ..", - "verify:web": "npm run build", - "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", - "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format", - "eslint": "eslint . --ext ts", - "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java", - "swiftlint": "node-swiftlint", - "docgen": "docgen --api PulseEditorCapacitorPlugin --output-readme README.md --output-json dist/docs.json", - "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs", - "clean": "rimraf ./dist", - "watch": "tsc --watch", - "prepublishOnly": "npm run build" - }, - "devDependencies": { - "@capacitor/android": "^7.0.0", - "@capacitor/core": "^7.0.0", - "@capacitor/docgen": "^0.3.0", - "@capacitor/ios": "^7.0.0", - "@ionic/eslint-config": "^0.4.0", - "@ionic/prettier-config": "^4.0.0", - "@ionic/swiftlint-config": "^2.0.0", - "eslint": "^8.57.0", - "prettier": "^3.4.2", - "prettier-plugin-java": "^2.6.6", - "rimraf": "^6.0.1", - "rollup": "^4.30.1", - "swiftlint": "^2.0.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@capacitor/core": ">=7.0.0" - }, - "prettier": "@ionic/prettier-config", - "swiftlint": "@ionic/swiftlint-config", - "eslintConfig": { - "extends": "@ionic/eslint-config/recommended" - }, - "capacitor": { - "ios": { - "src": "ios" - }, - "android": { - "src": "android" - } - } -} diff --git a/capacitor-plugin/rollup.config.mjs b/capacitor-plugin/rollup.config.mjs deleted file mode 100644 index 8cc2a1a1..00000000 --- a/capacitor-plugin/rollup.config.mjs +++ /dev/null @@ -1,22 +0,0 @@ -export default { - input: 'dist/esm/index.js', - output: [ - { - file: 'dist/plugin.js', - format: 'iife', - name: 'capacitorPulseEditorCapacitor', - globals: { - '@capacitor/core': 'capacitorExports', - }, - sourcemap: true, - inlineDynamicImports: true, - }, - { - file: 'dist/plugin.cjs.js', - format: 'cjs', - sourcemap: true, - inlineDynamicImports: true, - }, - ], - external: ['@capacitor/core'], -}; diff --git a/capacitor-plugin/src/definitions.ts b/capacitor-plugin/src/definitions.ts deleted file mode 100644 index 0a3941f8..00000000 --- a/capacitor-plugin/src/definitions.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface PulseEditorCapacitorPlugin { - echo(options: { value: string }): Promise<{ value: string }>; - startManageStorageIntent(): Promise; - isManageStoragePermissionGranted(): Promise<{ isGranted: boolean }>; -} diff --git a/capacitor-plugin/src/index.ts b/capacitor-plugin/src/index.ts deleted file mode 100644 index 8ea4deb7..00000000 --- a/capacitor-plugin/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { registerPlugin } from '@capacitor/core'; - -import type { PulseEditorCapacitorPlugin } from './definitions'; - -const PulseEditorCapacitor = registerPlugin('PulseEditorCapacitor', { - web: () => import('./web').then((m) => new m.PulseEditorCapacitorWeb()), -}); - -export * from './definitions'; -export { PulseEditorCapacitor }; diff --git a/capacitor-plugin/src/web.ts b/capacitor-plugin/src/web.ts deleted file mode 100644 index 9b31b7ef..00000000 --- a/capacitor-plugin/src/web.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { WebPlugin } from '@capacitor/core'; - -import type { PulseEditorCapacitorPlugin } from './definitions'; - -export class PulseEditorCapacitorWeb extends WebPlugin implements PulseEditorCapacitorPlugin { - async echo(options: { value: string }): Promise<{ value: string }> { - console.log('ECHO', options); - return options; - } - - async startManageStorageIntent(): Promise { - throw new Error('Method not implemented.'); - } - - async isManageStoragePermissionGranted(): Promise<{ isGranted: boolean }> { - throw new Error('Method not implemented.'); - } -} diff --git a/capacitor-plugin/tsconfig.json b/capacitor-plugin/tsconfig.json deleted file mode 100644 index f2e88e6a..00000000 --- a/capacitor-plugin/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "allowUnreachableCode": false, - "declaration": true, - "esModuleInterop": true, - "inlineSources": true, - "lib": ["dom", "es2017"], - "module": "esnext", - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "outDir": "dist/esm", - "pretty": true, - "sourceMap": true, - "strict": true, - "target": "es2017" - }, - "files": ["src/index.ts"] -} diff --git a/desktop/lib/node-pty-server.js b/desktop/lib/node-pty-server.js index 0296b9ae..7f36d5df 100644 --- a/desktop/lib/node-pty-server.js +++ b/desktop/lib/node-pty-server.js @@ -37,7 +37,7 @@ const handleTerminalConnection = (ws) => { } }); - ptyProcess.on("data", (rawOutput) => { + ptyProcess.onData((rawOutput) => { ws.send(JSON.stringify({ type: "output", payload: rawOutput })); }); @@ -68,4 +68,6 @@ export function createTerminalServer() { server.listen(port, () => { console.log(`HTTP and WebSocket server is running on port ${port}`); }); + + return server; } diff --git a/desktop/main.mjs b/desktop/main.mjs index 8cf17e1f..c9f8a2ee 100644 --- a/desktop/main.mjs +++ b/desktop/main.mjs @@ -35,6 +35,8 @@ serve({ }); let mainWindow = null; +let sharedSession = null; +let terminalServer = null; function createMainWindow() { const win = new BrowserWindow({ @@ -42,6 +44,7 @@ function createMainWindow() { height: 600, webPreferences: { preload: path.join(__dirname, "preload.mjs"), + session: sharedSession, }, titleBarOverlay: { color: "#00000000", @@ -277,29 +280,39 @@ async function handleLogin(event) { webPreferences: { nodeIntegration: false, contextIsolation: true, - session: session.defaultSession, // ← important + // login window and main window will automatically share cookies — no manual copying needed: + session: sharedSession, // ← important }, }); - const signinUrl = "https://pulse-editor.com/api/auth/signin"; + const signinUrl = app.isPackaged + ? "https://pulse-editor.com/api/auth/signin" + : "https://localhost:8080/api/auth/signin"; await loginWindow.loadURL(signinUrl); const loginSession = loginWindow.webContents.session; + // clear login window cookies before starting login + await clearLoginCookies(loginSession); + const interval = setInterval(async () => { try { - const cookies = await loginSession.cookies.get({ name: cookieName }); + const cookies = await sharedSession.cookies.get({ name: cookieName }); + + // If the cookie is present, reload the main window to reflect logged-in state if (cookies.length > 0) { clearInterval(interval); + mainWindow.reload(); + + // Check if cookies are present in the main window session + const mainCookies = await sharedSession.cookies.get({ + name: cookieName, + }); + // Login successful loginWindow.close(); console.log(`Login successful, cookie "${cookieName}" present.`); - - // Reload main window - if (mainWindow) { - mainWindow.reload(); - } } } catch (err) { console.error("Error checking cookie:", err); @@ -308,21 +321,8 @@ async function handleLogin(event) { } async function handleLogout() { - const mainSession = session.defaultSession; - const cookieName = "pulse-editor.session-token"; - try { - const cookies = await mainSession.cookies.get({ name: cookieName }); - - for (const cookie of cookies) { - const url = cookie.domain.startsWith(".") - ? `https://${cookie.domain.slice(1)}${cookie.path}` - : `https://${cookie.domain}${cookie.path}`; - - await mainSession.cookies.remove(url, cookie.name); - } - - console.log(`Cookie "${cookieName}" removed. Logout successful.`); + await clearLoginCookies(sharedSession); // Reload main window if (mainWindow) { @@ -337,10 +337,34 @@ async function handleLogout() { } } +async function clearLoginCookies(currentSession) { + // await currentSession.clearStorageData({ storages: ["cookies"] }); + + const cookieName = "pulse-editor.session-token"; + + const cookies = await currentSession.cookies.get({ + name: cookieName, + }); + + for (const cookie of cookies) { + // Determine the scheme (secure cookies use https) + const protocol = cookie.secure ? "https://" : "http://"; + + // Build the URL using domain and path + // Note: cookie.domain may start with a leading dot, remove it + const domain = cookie.domain.startsWith(".") + ? cookie.domain.slice(1) + : cookie.domain; + + const url = `${protocol}${domain}${cookie.path}`; + await currentSession.cookies.remove(url, cookie.name); + } +} + let isCreatedTerminal = false; function handleCreateTerminal(event) { if (!isCreatedTerminal) { - createTerminalServer(); + terminalServer = createTerminalServer(); isCreatedTerminal = true; } @@ -348,6 +372,9 @@ function handleCreateTerminal(event) { } app.whenReady().then(() => { + sharedSession = session.defaultSession; + console.log("Shared session path:", sharedSession.storagePath); + ipcMain.handle("select-dir", handleSelectDir); ipcMain.handle("select-file", handleSelectFile); @@ -370,8 +397,8 @@ app.whenReady().then(() => { ipcMain.handle("copy-files", handleCopyFiles); - ipcMain.handle("load-settings", handleLoadSettings); - ipcMain.handle("save-settings", handleSaveSettings); + ipcMain.handle("get-persistent-settings", handleLoadSettings); + ipcMain.handle("set-persistent-settings", handleSaveSettings); ipcMain.handle("get-installation-path", handleGetInstallationPath); @@ -387,4 +414,9 @@ app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } + + // Close terminal server if running + if (isCreatedTerminal) { + terminalServer.close(); + } }); diff --git a/desktop/package-lock.json b/desktop/package-lock.json index c992641e..aca031c9 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -12,7 +12,7 @@ "fs-extra": "^7.0.1", "ignore": "^5.3.2", "node-addon-api": "^7.1.1", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.2" }, "devDependencies": { @@ -4256,9 +4256,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta9", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta9.tgz", - "integrity": "sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==", + "version": "1.1.0-beta37", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta37.tgz", + "integrity": "sha512-Ys8AW98Atyu9cLV5QLQshvSTF+YMDksVi2ULkYNPAKLGzaDQbXuOEjStg4ZZuL4c8saOTh1+2PNlCoyuagBr1Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index a123f5c3..205170ad 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -15,7 +15,7 @@ "fs-extra": "^7.0.1", "ignore": "^5.3.2", "node-addon-api": "^7.1.1", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.2" }, "devDependencies": { diff --git a/desktop/preload.mjs b/desktop/preload.mjs index 2f49128d..ab56b490 100644 --- a/desktop/preload.mjs +++ b/desktop/preload.mjs @@ -1,6 +1,7 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { + // #region Platform API selectDir: () => ipcRenderer.invoke("select-dir"), selectFile: (fileExtension) => ipcRenderer.invoke("select-file", fileExtension), @@ -25,13 +26,17 @@ contextBridge.exposeInMainWorld("electronAPI", { copyFiles: (from, to) => ipcRenderer.invoke("copy-files", from, to), - loadSettings: () => ipcRenderer.invoke("load-settings"), - saveSettings: (settings) => ipcRenderer.invoke("save-settings", settings), + getPersistentSettings: () => ipcRenderer.invoke("get-persistent-settings"), + setPersistentSettings: (settings) => ipcRenderer.invoke("set-persistent-settings", settings), getInstallationPath: () => ipcRenderer.invoke("get-installation-path"), createTerminal: () => ipcRenderer.invoke("create-terminal"), + // #endregion + + // #region Auth API login: () => ipcRenderer.invoke("login"), logout: () => ipcRenderer.invoke("logout"), + // #endregion }); diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore index 7d82a7ca..89399e18 100644 --- a/mobile/android/.gitignore +++ b/mobile/android/.gitignore @@ -56,7 +56,7 @@ captures/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks -#*.keystore +*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 9ae01b72..73b79861 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "v0.0.1-alpha" + versionName "v0.1.1-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 6d702635..f80517b2 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -10,12 +10,14 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-community-safe-area') + implementation project(':capacitor-app') + implementation project(':capacitor-browser') implementation project(':capacitor-filesystem') implementation project(':capacitor-keyboard') + implementation project(':capacitor-preferences') implementation project(':capacitor-screen-orientation') implementation project(':capacitor-status-bar') implementation project(':capawesome-capacitor-file-picker') - implementation project(':pulse-editor-capacitor-plugin') } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 479d6703..c60fa1c4 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + @@ -10,7 +10,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" - android:usesCleartextTraffic="true"> + android:networkSecurityConfig="@xml/network_security_config"> + + + + + + + + + + + + + + + - + @@ -43,7 +60,4 @@ - - - + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/xml/network_security_config.xml b/mobile/android/app/src/main/res/xml/network_security_config.xml index de61259a..bb6ab93d 100644 --- a/mobile/android/app/src/main/res/xml/network_security_config.xml +++ b/mobile/android/app/src/main/res/xml/network_security_config.xml @@ -1,6 +1,9 @@ - - localhost - - + + + + + + + \ No newline at end of file diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index f2bf9f46..d224628d 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -3,7 +3,13 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') include ':capacitor-community-safe-area' -project(':capacitor-community-safe-area').projectDir = new File('../../node_modules/@capacitor-community/safe-area/android') +project(':capacitor-community-safe-area').projectDir = new File('../node_modules/@capacitor-community/safe-area/android') + +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android') + +include ':capacitor-browser' +project(':capacitor-browser').projectDir = new File('../../node_modules/@capacitor/browser/android') include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android') @@ -11,6 +17,9 @@ project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capa include ':capacitor-keyboard' project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android') +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../../node_modules/@capacitor/preferences/android') + include ':capacitor-screen-orientation' project(':capacitor-screen-orientation').projectDir = new File('../../node_modules/@capacitor/screen-orientation/android') @@ -19,6 +28,3 @@ project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capa include ':capawesome-capacitor-file-picker' project(':capawesome-capacitor-file-picker').projectDir = new File('../../node_modules/@capawesome/capacitor-file-picker/android') - -include ':pulse-editor-capacitor-plugin' -project(':pulse-editor-capacitor-plugin').projectDir = new File('../../capacitor-plugin/android') diff --git a/mobile/capacitor.config.ts b/mobile/capacitor.config.ts index a017e10b..9110b9d4 100644 --- a/mobile/capacitor.config.ts +++ b/mobile/capacitor.config.ts @@ -17,6 +17,9 @@ const config: CapacitorConfig = { SafeArea: { enabled: true, }, + CapacitorHttp: { + enabled: true, + }, }, }; diff --git a/mobile/package.json b/mobile/package.json index b32ff734..48b95d96 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -5,18 +5,20 @@ "type": "module", "scripts": { "android-dev": "node dev.js", - "android-build": "npx cap run android" + "android-run": "npx cap run android" }, "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.2.0", + "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.2.0", "@capacitor/core": "^7.2.0", "@capacitor/filesystem": "^7.0.1", "@capacitor/keyboard": "^7.0.1", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.1", "@capacitor/status-bar": "^7.0.1", - "@capawesome/capacitor-file-picker": "^7.0.1", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin" + "@capawesome/capacitor-file-picker": "^7.0.1" } } diff --git a/npm-packages/cli/package.json b/npm-packages/cli/package.json index 46768f7a..dc20f3bb 100644 --- a/npm-packages/cli/package.json +++ b/npm-packages/cli/package.json @@ -1,9 +1,9 @@ { "name": "@pulse-editor/cli", - "version": "0.1.0-beta.5", + "version": "0.1.0-beta.8", "license": "MIT", "bin": { - "pulse": "./dist/cli.js" + "pulse": "dist/cli.js" }, "type": "module", "engines": { @@ -60,4 +60,4 @@ } }, "prettier": "@vdemedes/prettier-config" -} \ No newline at end of file +} diff --git a/npm-packages/cli/source/components/commands/create.tsx b/npm-packages/cli/source/components/commands/create.tsx index 8ae10632..7f9ac339 100644 --- a/npm-packages/cli/source/components/commands/create.tsx +++ b/npm-packages/cli/source/components/commands/create.tsx @@ -165,8 +165,9 @@ export default function Create({cli}: {cli: Result}) { try { await execa(`npm install`, { cwd: path.join(process.cwd(), name), + shell: true, }); - } catch (error) { + } catch (error: any) { setCreateMessage( ❌ Failed to install dependencies. Please check your internet diff --git a/npm-packages/cli/source/components/commands/publish.tsx b/npm-packages/cli/source/components/commands/publish.tsx index e364175d..b3b43f5f 100644 --- a/npm-packages/cli/source/components/commands/publish.tsx +++ b/npm-packages/cli/source/components/commands/publish.tsx @@ -54,13 +54,23 @@ export default function Publish({cli}: {cli: Result}) { // Build the extension useEffect(() => { async function buildExtension() { + setIsBuilding(true); try { - setIsBuilding(true); await $`npm run build`; - // Zip the dist folder + } + catch (error) { + setIsBuildingError(true); + setIsBuilding(false); + setFailureMessage('Build failed. Please run `npm run build` to see the error.'); + return; + } + // Zip the dist folder + try { await $({cwd: 'dist'})`zip -r ../node_modules/@pulse-editor/dist.zip *`; } catch (error) { setIsBuildingError(true); + setIsBuilding(false); + setFailureMessage('Failed to zip the build output.'); return; } finally { setIsBuilding(false); @@ -93,8 +103,8 @@ export default function Publish({cli}: {cli: Result}) { // Send the file to the server const res = await fetch( cli.flags.dev - ? 'https://localhost:8080/api/extension/publish' - : 'https://pulse-editor.com/api/extension/publish', + ? 'https://localhost:8080/api/app/publish' + : 'https://pulse-editor.com/api/app/publish', { method: 'POST', headers: { diff --git a/package-lock.json b/package-lock.json index 91cd85a4..90bb5aa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "workspaces": [ "web", "mobile", - "capacitor-plugin", "vscode-extension", "npm-packages/react-api", "npm-packages/shared-utils" @@ -25,6 +24,7 @@ "capacitor-plugin": { "name": "@pulse-editor/capacitor-plugin", "version": "0.0.1", + "extraneous": true, "license": "MIT", "devDependencies": { "@capacitor/android": "^7.0.0", @@ -46,90 +46,22 @@ "@capacitor/core": ">=7.0.0" } }, - "capacitor-plugin/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "capacitor-plugin/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "capacitor-plugin/node_modules/prettier-plugin-java": { - "version": "2.6.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "java-parser": "2.3.3", - "lodash": "4.17.21" - }, - "peerDependencies": { - "prettier": "^3.0.0" - } - }, - "capacitor-plugin/node_modules/rimraf": { - "version": "6.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "mobile": { "name": "@pulse-editor/mobile", "version": "0.0.1", "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.2.0", + "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.2.0", "@capacitor/core": "^7.2.0", "@capacitor/filesystem": "^7.0.1", "@capacitor/keyboard": "^7.0.1", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.1", "@capacitor/status-bar": "^7.0.1", - "@capawesome/capacitor-file-picker": "^7.0.1", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin" + "@capawesome/capacitor-file-picker": "^7.0.1" } }, "mobile/node_modules/@capacitor-community/safe-area": { @@ -1830,6 +1762,24 @@ "@capacitor/core": "^7.4.0" } }, + "node_modules/@capacitor/app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-7.1.0.tgz", + "integrity": "sha512-W7m09IWrUjZbo7AKeq+rc/KyucxrJekTBg0l4QCm/yDtCejE3hebxp/W2esU26KKCzMc7H3ClkUw32E9lZkwRA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, + "node_modules/@capacitor/browser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-7.0.2.tgz", + "integrity": "sha512-5kySTunCtH+2sezmTjgDfwvspW7GW/hslQECZeLIRM2qefnxjGTc3fmCTeILYK5EuvcxMs+8sF5BhmzzKqOzuQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.4.3.tgz", @@ -1963,47 +1913,6 @@ "tslib": "^2.1.0" } }, - "node_modules/@capacitor/docgen": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@capacitor/docgen/-/docgen-0.3.0.tgz", - "integrity": "sha512-WPggobo5Ql70F+2xOIUwNSApJXaL9F/9+Al6B+sNuSAmcg484OAksyUPKgiynF4BVlxeY5a0sDkgdVkmmA3ElQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^14.18.0", - "colorette": "^2.0.20", - "github-slugger": "^1.5.0", - "minimist": "^1.2.8", - "typescript": "~4.2.4" - }, - "bin": { - "docgen": "bin/docgen" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@capacitor/docgen/node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@capacitor/docgen/node_modules/typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@capacitor/filesystem": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-7.1.4.tgz", @@ -2016,16 +1925,6 @@ "@capacitor/core": ">=7.0.0" } }, - "node_modules/@capacitor/ios": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.4.3.tgz", - "integrity": "sha512-VNm7cHODgh3KK/4ZC2rXU9gBlvHii/mYFLI+XMXwq24nhB679QxHhz+pUuI7PatYoM2q4MAL0NR/dRgehKCaSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@capacitor/core": "^7.4.0" - } - }, "node_modules/@capacitor/keyboard": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.3.tgz", @@ -2035,6 +1934,15 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/preferences": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.2.tgz", + "integrity": "sha512-JVCy0/oc6RsRencLOZ8rMqjNxAlHs7awPJU/MXqangsJ48oO2PnYGHfCvci6WgIJlqyC0QhvWZaO1BR1lVkHWQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/screen-orientation": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@capacitor/screen-orientation/-/screen-orientation-7.0.2.tgz", @@ -4764,39 +4672,6 @@ "node": ">=16.0.0" } }, - "node_modules/@ionic/eslint-config": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.4.0.tgz", - "integrity": "sha512-L8OXY29D3iGqNtteFj0iz3eoZIVgokBiVjCO8WMssNZa4GTHjYsase0rC9ASXGefMnLJu6rbNl3Gbx7NNxJRZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.0" - }, - "peerDependencies": { - "eslint": ">=7" - } - }, - "node_modules/@ionic/prettier-config": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-4.0.0.tgz", - "integrity": "sha512-0DqL6CggVdgeJAWOLPUT73rF1VD5p0tVlCpC5GXz5vTIUBxNwsJ5085Q7wXjKiE5Odx3aOHGTcuRWCawFsLFag==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^2.4.0 || ^3.0.0" - } - }, - "node_modules/@ionic/swiftlint-config": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ionic/swiftlint-config/-/swiftlint-config-2.0.0.tgz", - "integrity": "sha512-TXy76ALSKhUZzBziHz7aoEtSQwHofBIDRNzM9x4sndtC7fefbZsBw3UgGwXFTOc7hoj72EAGyqZNUhj9LlhaNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@ionic/utils-array": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", @@ -6290,10 +6165,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@pulse-editor/capacitor-plugin": { - "resolved": "capacitor-plugin", - "link": true - }, "node_modules/@pulse-editor/mobile": { "resolved": "mobile", "link": true @@ -11313,33 +11184,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -11946,23 +11790,6 @@ "node": ">=4" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -12285,19 +12112,6 @@ } } }, - "node_modules/eslint-config-prettier": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", - "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -13351,13 +13165,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "dev": true, - "license": "ISC" - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -15623,13 +15430,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -20668,25 +20468,6 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -23161,22 +22942,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swiftlint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/swiftlint/-/swiftlint-2.0.0.tgz", - "integrity": "sha512-MMVuyZ4/6WcIJlk0z6GM0pZjRuwnyUJqRPbJBFW3oACN/qjAvRbolCWEu+zE2MycF/cEgqfUpI+oLECNfjfOJA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@ionic/utils-fs": "^3.1.7", - "@ionic/utils-subprocess": "^3.0.1", - "cosmiconfig": "^9.0.0" - }, - "bin": { - "node-swiftlint": "bin.js" - } - }, "node_modules/swr": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", @@ -26522,14 +26287,17 @@ }, "web": { "name": "@pulse-editor/web", - "version": "0.1.1-alpha.13", + "version": "0.1.1-beta.0", "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", + "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.4.3", "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", "@capacitor/keyboard": "^7.0.3", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.2", "@capacitor/status-bar": "^7.0.3", "@capawesome/capacitor-file-picker": "^7.2.0", @@ -26539,7 +26307,6 @@ "@langchain/community": "^0.3.49", "@langchain/core": "^0.3.66", "@langchain/openai": "^0.6.3", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin", "@pulse-editor/shared-utils": "^0.1.1-beta.55", "@ricky0123/vad-web": "^0.0.28", "@vercel/analytics": "^1.5.0", diff --git a/package.json b/package.json index caaa7494..7195c3d1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "workspaces": [ "web", "mobile", - "capacitor-plugin", "vscode-extension", "npm-packages/react-api", "npm-packages/shared-utils" @@ -17,12 +16,11 @@ "desktop-dev": "npm --prefix ./desktop run dev-https", "desktop-build": "npm run web-build && npm --prefix ./desktop run build", "android-dev": "npm run web-build && npm run android-dev --workspace=mobile", - "android-build": "npm run web-build && npm run android-build --workspace=mobile", + "android-run": "npm run web-build && npm run android-run --workspace=mobile", "react-api-build": "npm run build --workspace=npm-packages/react-api", "shared-utils-build": "npm run build --workspace=npm-packages/shared-utils", "cli-dev": "npm run dev --workspace=cli", - "cli-build": "npm run build --workspace=cli", - "capacitor-plugin-build": "npm run build --workspace=capacitor-plugin" + "cli-build": "npm run build --workspace=cli" }, "devDependencies": { "@changesets/cli": "^2.29.4", @@ -30,4 +28,4 @@ "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-tailwindcss": "^0.6.14" } -} +} \ No newline at end of file diff --git a/remote-instance/.env.example b/remote-instance/.env.example deleted file mode 100644 index a29b64f2..00000000 --- a/remote-instance/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -SSL_CERT_PATH=path_to_cert -SSL_KEY_PATH=path_to_key -# You can modify this to custom frontend deployment URL -FRONTEND_URL=https://editor.pulse-editor.com diff --git a/remote-instance/src/index.ts b/remote-instance/src/index.ts deleted file mode 100644 index e4cce2ea..00000000 --- a/remote-instance/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createTerminalServer } from "./servers/node-pty"; -import { createAPIServer } from "./servers/api-server"; - -/* Create servers */ -createAPIServer().then((server) => { - // After API server is created, the terminal server can use it - createTerminalServer(server); -}); diff --git a/remote-instance/src/servers/api-server/index.ts b/remote-instance/src/servers/api-server/index.ts deleted file mode 100644 index 6b03be52..00000000 --- a/remote-instance/src/servers/api-server/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import express from "express"; -import https from "https"; -import http from "http"; -import fs from "fs"; -import dotenv from "dotenv"; -import { handlePlatformAPIRequest } from "./platform-api/handler"; - -dotenv.config(); - -const app = express(); -const HOST = "0.0.0.0"; -const HTTP_SERVER_PORT = 6080; -const HTTPS_SERVER_PORT = 6443; -const certPath = process.env.SSL_CERT_PATH; -const keyPath = process.env.SSL_KEY_PATH; - -export async function createAPIServer() { - await createEndpoints(app); - - if ( - certPath && - keyPath && - fs.existsSync(certPath) && - fs.existsSync(keyPath) - ) { - const server = https.createServer( - { - key: fs.readFileSync(keyPath), - cert: fs.readFileSync(certPath), - }, - app - ); - server.listen(HTTPS_SERVER_PORT, HOST, () => { - console.log(`HTTPS server is running on port ${HTTPS_SERVER_PORT}`); - }); - return server; - } else { - const server = http.createServer(app); - server.listen(HTTP_SERVER_PORT, HOST, () => { - console.log(`HTTP server is running on port ${HTTP_SERVER_PORT}`); - }); - return server; - } -} - -async function createEndpoints(app: express.Express) { - app.use(express.json()); - - app.get("/:instanceId/", (req, res) => { - const instanceId = req.params.instanceId; - if (instanceId !== process.env.INSTANCE_ID) { - return res.status(400).send("Invalid instance ID"); - } - // Get the requested URL - const serverUrl = req.protocol + "://" + req.get("host") + req.originalUrl; - - // Redirect to https://editor.pulse-editor.com and append - // this instance's URL as a query parameter - const url = new URL( - process.env.FRONTEND_URL ?? "https://editor.pulse-editor.com" - ); - url.searchParams.append("instance", serverUrl); - res.redirect(url.toString()); - }); - - app.get("/:instanceId/test", (req, res) => { - const instanceId = req.params.instanceId; - if (instanceId !== process.env.INSTANCE_ID) { - return res.status(400).send("Invalid instance ID"); - } - res.send("Remote instance is running!"); - }); - - app.post("/:instanceId/platform-api", async (req, res) => { - const instanceId = req.params.instanceId; - if (instanceId !== process.env.INSTANCE_ID) { - return res.status(400).send("Invalid instance ID"); - } - - // Get json body - const body = req.body; - - console.log("Received platform API request:", body); - - const host = req.host; - - const result = await handlePlatformAPIRequest( - body, - host, - instanceId - ); - - // Process the request and send a response - if (result && result.error) { - res.status(400).json(result); - } else { - res.send(result); - } - }); -} diff --git a/remote-instance/src/servers/api-server/platform-api/handler.ts b/remote-instance/src/servers/api-server/platform-api/handler.ts deleted file mode 100644 index 4aae2ea1..00000000 --- a/remote-instance/src/servers/api-server/platform-api/handler.ts +++ /dev/null @@ -1,232 +0,0 @@ -import fs from "fs"; -import ignore from "ignore"; -import path from "path"; - -// Define a safe root directory for projects. Can be overridden by env or configured as needed. -const PROJECTS_ROOT = process.env.PROJECTS_ROOT ?? "/srv/projects"; - -// Utility to resolve and validate user-supplied uri inside PROJECTS_ROOT -function getSafePath(uri: string): string { - // Prevent empty/undefined input - if (!uri || typeof uri !== 'string') { - throw new Error("Invalid project path"); - } - // Resolve against the root directory - const resolved = path.resolve(PROJECTS_ROOT, uri); - // Use fs.realpathSync to follow symlinks - let normalized; - try { - normalized = fs.realpathSync(resolved); - } catch { - // If path does not exist yet (e.g., on creation), just use resolved. - normalized = resolved; - } - // Ensure the normalized path is inside the root - if (!normalized.startsWith(PROJECTS_ROOT)) { - throw new Error("Access to paths outside projects root denied"); - } - return normalized; -} -// List all folders in a path -async function handleListProjects(uri: string) { - const rootPath = getSafePath(uri); - const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); - const folders = files - .filter((file) => file.isDirectory()) - .map((file) => file.name) - .map((projectName) => ({ - name: projectName, - ctime: fs.statSync(path.join(rootPath, projectName)).ctime, - })); - - return folders; -} - -async function listPathContent( - uri: string, - options: any, - baseUri: string | undefined = undefined -) { - const rootPath = getSafePath(uri); - const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); - - const promise: Promise[] = files - // Filter by file type - .filter( - (file) => - (options?.include === "folders" && file.isDirectory()) || - (options?.include === "files" && file.isFile()) || - options?.include === "all" - ) - // Filter by gitignore - .filter((file) => { - if (!options?.gitignore) { - return true; - } - const ig = ignore().add(options.gitignore); - - const filePath = baseUri - ? path.relative(baseUri, path.join(uri, file.name)) - : file.name; - - const isIgnored = ig.ignores(filePath); - - return !isIgnored; - }) - .map(async (file) => { - const name = file.name; - const absoluteUri = path.join(rootPath, name); - if (file.isDirectory()) { - return { - name: name, - isFolder: true, - subDirItems: options.isRecursive - ? await listPathContent(absoluteUri, options, baseUri ?? uri) - : [], - uri: absoluteUri.replace(/\\/g, "/"), - }; - } - - return { - name, - isFolder: false, - uri: absoluteUri.replace(/\\/g, "/"), - }; - }); - - return Promise.all(promise); -} - -// Discover the content of a project -async function handleListPathContent(uri: string, options: any) { - return await listPathContent(uri, options); -} - -async function handleCreateProject(uri: string) { - // Create a folder at the validated path - await fs.promises.mkdir(getSafePath(uri)); -} - -async function handleCreateFolder(uri: string) { - // Create a folder at the validated path - await fs.promises.mkdir(getSafePath(uri)); -} - -async function handleCreateFile(uri: string) { - // Create a file at the validated path - await fs.promises.writeFile(getSafePath(uri), ""); -} - -async function handleRename(oldUri: string, newUri: string) { - await fs.promises.rename(getSafePath(oldUri), getSafePath(newUri)); -} - -async function handleDelete(uri: string) { - await fs.promises.rm(getSafePath(uri), { recursive: true, force: true }); -} - -async function handleHasPath(uri: string) { - return fs.existsSync(getSafePath(uri)); -} - -async function handleReadFile(uri: string) { - // Read the file at validated path - const data = await fs.promises.readFile(getSafePath(uri), "utf-8"); - - return data; -} - -async function handleWriteFile(data: any, uri: string) { - // Write the data at validated path - const safePath = getSafePath(uri); - // create parent directory if it doesn't exist - const dir = path.dirname(safePath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(safePath, data); -} - -async function handleCopyFiles(from: string, to: string) { - // Copy the files from the validated from path to the validated to path - await fs.promises.cp(getSafePath(from), getSafePath(to), { recursive: true }); -} - -async function handleLoadSettings() { - if (fs.existsSync(settingsPath)) { - const data = fs.readFileSync(settingsPath, "utf-8"); - return JSON.parse(data); - } - return {}; -} - -async function handleSaveSettings(settings: any) { - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); -} - -async function handleGetInstallationPath() { - const uri = "~/pulse-editor"; - return uri; -} - -export async function handlePlatformAPIRequest( - data: any, - host: string, - instanceId: string -): Promise { - const { operation, args } = data; - - if (operation === "select-dir") { - throw new Error("Method not implemented."); - } else if (operation === "select-file") { - throw new Error("Method not implemented."); - } else if (operation === "list-projects") { - const { uri }: { uri: string } = args; - return await handleListProjects(uri); - } else if (operation === "list-path-content") { - const { uri, options }: { uri: string; options?: any } = args; - - return await handleListPathContent(uri, options); - } else if (operation === "create-project") { - const { uri }: { uri: string } = args; - await handleCreateProject(uri); - } else if (operation === "create-folder") { - const { uri }: { uri: string } = args; - await handleCreateFolder(uri); - } else if (operation === "create-file") { - const { uri }: { uri: string } = args; - await handleCreateFile(uri); - } else if (operation === "rename") { - const { oldUri, newUri }: { oldUri: string; newUri: string } = args; - await handleRename(oldUri, newUri); - } else if (operation === "delete") { - const { uri }: { uri: string } = args; - await handleDelete(uri); - } else if (operation === "has-path") { - const { uri }: { uri: string } = args; - return await handleHasPath(uri); - } else if (operation === "read-file") { - const { uri }: { uri: string } = args; - return handleReadFile(uri); - } else if (operation === "write-file") { - const { data, uri }: { data: any; uri: string } = args; - await handleWriteFile(data, uri); - } else if (operation === "copy-files") { - const { from, to }: { from: string; to: string } = args; - await handleCopyFiles(from, to); - } else if (operation === "get-persistent-settings") { - return handleLoadSettings(); - } else if (operation === "set-persistent-settings") { - const { settings }: { settings: any } = args; - await handleSaveSettings(settings); - } else if (operation === "reset-persistent-settings") { - await handleSaveSettings({}); - } else if (operation === "get-installation-path") { - return await handleGetInstallationPath(); - } else if (operation === "create-terminal") { - return `${host}/${instanceId}/terminal/ws`; - } - // Do not reflect input data back to the client, return an explicit error message. - return { error: "Unknown operation" }; -} diff --git a/remote-instance/.dockerignore b/remote-workspace/.dockerignore similarity index 100% rename from remote-instance/.dockerignore rename to remote-workspace/.dockerignore diff --git a/remote-workspace/.env.example b/remote-workspace/.env.example new file mode 100644 index 00000000..5c00bfd2 --- /dev/null +++ b/remote-workspace/.env.example @@ -0,0 +1,9 @@ +SSL_CERT_PATH=path_to_cert +SSL_KEY_PATH=path_to_key +# You can modify this to custom frontend deployment URL +FRONTEND_URL=https://web.pulse-editor.com +# Name of the workspace instance +WORKSPACE_ID=test-workspace +# Port for the server to listen on. +# Default to 6080 +SERVER_PORT=6080 diff --git a/remote-instance/.gitignore b/remote-workspace/.gitignore similarity index 100% rename from remote-instance/.gitignore rename to remote-workspace/.gitignore diff --git a/remote-instance/README.md b/remote-workspace/README.md similarity index 100% rename from remote-instance/README.md rename to remote-workspace/README.md diff --git a/remote-instance/dockerfile b/remote-workspace/dockerfile similarity index 74% rename from remote-instance/dockerfile rename to remote-workspace/dockerfile index ecd9727b..d76a87d3 100644 --- a/remote-instance/dockerfile +++ b/remote-workspace/dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:latest ENV NODE_VERSION=20 -WORKDIR /app +WORKDIR /pulse-editor/server # install curl RUN apt update && apt install -y curl make python3 build-essential @@ -12,11 +12,17 @@ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | b # set env ENV NVM_DIR=/root/.nvm -COPY . /app +COPY . /pulse-editor/server # install node RUN bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && npm install && npm run build" +# install pulse cli +RUN bash -c "source $NVM_DIR/nvm.sh && npm install -g @pulse-editor/cli@beta" + +# install git, zip, unzip +RUN apt install -y git zip unzip + # Generate self-signed certificate # RUN bash -c "./utils/generate-self-signed.sh" diff --git a/remote-instance/package-lock.json b/remote-workspace/package-lock.json similarity index 98% rename from remote-instance/package-lock.json rename to remote-workspace/package-lock.json index 002ef76a..17b57ef4 100644 --- a/remote-instance/package-lock.json +++ b/remote-workspace/package-lock.json @@ -1,19 +1,18 @@ { - "name": "pulse-editor-remote-instance", + "name": "pulse-editor-remote-workspace", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pulse-editor-remote-instance", + "name": "pulse-editor-remote-workspace", "version": "1.0.0", "license": "MIT", "dependencies": { - "@pulse-editor/shared-utils": "^0.1.1-alpha.24", "dotenv": "^17.2.0", "express": "^5.1.0", "ignore": "^7.0.5", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.3" }, "devDependencies": { @@ -466,11 +465,6 @@ "node": ">=18" } }, - "node_modules/@pulse-editor/shared-utils": { - "version": "0.1.1-alpha.24", - "resolved": "https://registry.npmjs.org/@pulse-editor/shared-utils/-/shared-utils-0.1.1-alpha.24.tgz", - "integrity": "sha512-aHSrc1Ntpvvs8npeSa543v0imjd/20UdXiJtLS5g2CsJAWAayzMjEU/nL6aWaE2rn6xThq8xgfy2Iu1Wr/SclQ==" - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1179,9 +1173,9 @@ "license": "MIT" }, "node_modules/node-pty": { - "version": "1.1.0-beta34", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta34.tgz", - "integrity": "sha512-RraDtX9RS1G1I5iO7e4YIOIA4arzd4ZVCD4mZr7+szaNupoTg9fxDCRr0EanqS0Qlzgm3PIdHNbPmblJguJuyg==", + "version": "1.1.0-beta37", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta37.tgz", + "integrity": "sha512-Ys8AW98Atyu9cLV5QLQshvSTF+YMDksVi2ULkYNPAKLGzaDQbXuOEjStg4ZZuL4c8saOTh1+2PNlCoyuagBr1Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/remote-instance/package.json b/remote-workspace/package.json similarity index 72% rename from remote-instance/package.json rename to remote-workspace/package.json index 2ddffcd7..862881d0 100644 --- a/remote-instance/package.json +++ b/remote-workspace/package.json @@ -1,7 +1,7 @@ { - "name": "pulse-editor-remote-instance", + "name": "pulse-editor-remote-workspace", "version": "1.0.0", - "description": "Remote instance backend for Pulse Editor", + "description": "Remote workspace backend for Pulse Editor", "license": "MIT", "author": "ClayPulse", "main": "src/index.ts", @@ -11,11 +11,10 @@ "start": "node dist/index.js" }, "dependencies": { - "@pulse-editor/shared-utils": "^0.1.1-alpha.24", "dotenv": "^17.2.0", "express": "^5.1.0", "ignore": "^7.0.5", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.3" }, "devDependencies": { diff --git a/remote-workspace/src/index.ts b/remote-workspace/src/index.ts new file mode 100644 index 00000000..cdb54667 --- /dev/null +++ b/remote-workspace/src/index.ts @@ -0,0 +1,68 @@ +import dotenv from "dotenv"; +import express from "express"; +import fs from "fs"; +import http from "http"; +import https from "https"; +import { getLocalNetworkIP } from "./lib/get-network"; +import { addAPIServer } from "./servers/api-server"; +import { addTerminalServer } from "./servers/node-pty"; + +dotenv.config(); + +const expressApp = express(); +const serverPort = process.env.SERVER_PORT + ? parseInt(process.env.SERVER_PORT) + : 6080; +const certPath = process.env.SSL_CERT_PATH; +const keyPath = process.env.SSL_KEY_PATH; +const workspaceId = process.env.WORKSPACE_ID; +const frontendUrl = process.env.FRONTEND_URL ?? "https://web.pulse-editor.com"; + +async function startServers() { + if (!workspaceId) { + console.error("WORKSPACE_ID is not set in environment variables."); + process.exit(1); + } + + /* Create servers */ + const server = await createServer(); + const address = getLocalNetworkIP(); + + const isHttps = + certPath && keyPath && fs.existsSync(certPath) && fs.existsSync(keyPath) + ? true + : false; + + await addAPIServer(server, expressApp, workspaceId, serverPort, frontendUrl); + console.log( + `API server is running at ${isHttps ? "https" : "http"}://${address}:${serverPort}/${workspaceId}`, + ); + + await addTerminalServer(server, workspaceId); + console.log( + `Terminal server is running at ${isHttps ? "wss" : "ws"}://${address}:${serverPort}/${workspaceId}/terminal/ws`, + ); +} + +async function createServer() { + if ( + certPath && + keyPath && + fs.existsSync(certPath) && + fs.existsSync(keyPath) + ) { + const server = https.createServer( + { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath), + }, + expressApp, + ); + return server; + } else { + const server = http.createServer(expressApp); + return server; + } +} + +startServers(); diff --git a/remote-workspace/src/lib/get-network.ts b/remote-workspace/src/lib/get-network.ts new file mode 100644 index 00000000..a8a46f05 --- /dev/null +++ b/remote-workspace/src/lib/get-network.ts @@ -0,0 +1,14 @@ +import { networkInterfaces } from "os"; + +export function getLocalNetworkIP() { + const interfaces = networkInterfaces(); + for (const iface of Object.values(interfaces)) { + if (!iface) continue; + for (const config of iface) { + if (config.family === "IPv4" && !config.internal) { + return config.address; // Returns the first non-internal IPv4 address + } + } + } + return "localhost"; // Fallback +} \ No newline at end of file diff --git a/remote-workspace/src/servers/api-server/index.ts b/remote-workspace/src/servers/api-server/index.ts new file mode 100644 index 00000000..f39e7bb9 --- /dev/null +++ b/remote-workspace/src/servers/api-server/index.ts @@ -0,0 +1,78 @@ +import dotenv from "dotenv"; +import express from "express"; +import http from "http"; +import https from "https"; +import { handlePlatformAPIRequest } from "./platform-api/handler"; + +dotenv.config(); + +const HOST = "0.0.0.0"; + +export async function addAPIServer( + server: http.Server | https.Server, + expressApp: express.Express, + instanceId: string, + port: number, + frontendUrl: string, +) { + await createEndpoints(expressApp, instanceId, frontendUrl); + + server.listen(port, HOST); +} + +async function createEndpoints( + app: express.Express, + instanceId: string, + frontendUrl: string, +) { + app.use(express.json()); + + app.get("/:instanceId/", (req, res) => { + const id = req.params.instanceId; + if (id !== instanceId) { + return res.status(400).send("Invalid instance ID"); + } + // Get the requested URL + const serverUrl = req.protocol + "://" + req.get("host") + req.originalUrl; + + // Redirect to https://editor.pulse-editor.com and append + // this instance's URL as a query parameter + const url = new URL(frontendUrl); + url.searchParams.append("instance", serverUrl); + res.redirect(url.toString()); + }); + + app.get("/:instanceId/test", (req, res) => { + const id = req.params.instanceId; + if (id !== instanceId) { + return res.status(400).send("Invalid instance ID"); + } + res.send("Remote instance is running!"); + }); + + app.post("/:instanceId/platform-api", async (req, res) => { + const id = req.params.instanceId; + if (id !== instanceId) { + return res.status(400).send("Invalid instance ID"); + } + + // Get json body + const { operation, args } = req.body; + + const result = await handlePlatformAPIRequest( + { + operation, + args, + }, + req.get("host") ?? "", + instanceId, + ); + + // Process the request and send a response + if (result && result.error) { + res.status(400).json(result); + } else { + res.send(JSON.stringify(result)); + } + }); +} diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts new file mode 100644 index 00000000..e8cdea24 --- /dev/null +++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts @@ -0,0 +1,286 @@ +import fs from "fs"; +import ignore from "ignore"; +import path from "path"; + +// Define a safe root directory for projects. Can be overridden by env or configured as needed. + +const settingsPath = path.join("/pulse-editor", "settings.json"); + +export async function handlePlatformAPIRequest( + data: { + operation: string; + args: any; + }, + host: string, + instanceId: string, +): Promise { + const { operation, args } = data; + + switch (operation) { + case "select-dir": + // Folder picker is done via web interface + throw new Error("Method not implemented."); + case "select-file": + // File picker is done via web interface + throw new Error("Method not implemented."); + case "list-projects": { + const { uri }: { uri: string } = args; + return await handleListProjects(uri); + } + case "list-path-content": { + const { uri, options }: { uri: string; options?: any } = args; + return await handleListPathContent(uri, options); + } + case "create-project": { + const { uri }: { uri: string } = args; + await handleCreateProject(uri); + return; + } + case "delete-project": { + const { uri }: { uri: string } = args; + await handleDeleteProject(uri); + return; + } + case "update-project": { + const { + uri, + updatedInfo, + }: { + uri: string; + updatedInfo: { + name: string; + ctime?: Date; + }; + } = args; + await handleUpdateProject(uri, updatedInfo); + return; + } + case "create-folder": { + const { uri }: { uri: string } = args; + await handleCreateFolder(uri); + return; + } + case "create-file": { + const { uri }: { uri: string } = args; + await handleCreateFile(uri); + return; + } + case "rename": { + const { oldUri, newUri }: { oldUri: string; newUri: string } = args; + await handleRename(oldUri, newUri); + return; + } + case "delete": { + const { uri }: { uri: string } = args; + await handleDelete(uri); + return; + } + case "has-path": { + const { uri }: { uri: string } = args; + return await handleHasPath(uri); + } + case "read-file": { + const { uri }: { uri: string } = args; + return await handleReadFile(uri); + } + case "write-file": { + const { data, uri }: { data: any; uri: string } = args; + await handleWriteFile(data, uri); + return; + } + case "copy-files": { + const { from, to }: { from: string; to: string } = args; + await handleCopyFiles(from, to); + return; + } + case "get-persistent-settings": + return handleLoadSettings(); + case "set-persistent-settings": { + const { settings }: { settings: any } = args; + await handleSaveSettings(settings); + return; + } + case "get-installation-path": + return await handleGetInstallationPath(); + case "create-terminal": + return `${host}/${instanceId}/terminal/ws`; + default: + // Do not reflect input data back to the client, return an explicit error message. + return { error: "Unknown operation" }; + } +} + + +// List all folders in a path +async function handleListProjects(uri: string) { + const rootPath = uri; + const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); + const folders = files + .filter((file) => file.isDirectory()) + .map((file) => file.name) + .map((projectName) => ({ + name: projectName, + ctime: fs.statSync(path.join(rootPath, projectName)).ctime, + })); + + return folders; +} + +async function listPathContent( + uri: string, + options: any, + baseUri: string | undefined = undefined, +) { + const rootPath = uri; + const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); + + const promise: Promise[] = files + // Filter by file type + .filter( + (file) => + (options?.include === "folders" && file.isDirectory()) || + (options?.include === "files" && file.isFile()) || + options?.include === "all", + ) + // Filter by gitignore + .filter((file) => { + if (!options?.gitignore) { + return true; + } + const ig = ignore().add(options.gitignore); + + const filePath = baseUri + ? path.relative(baseUri, path.join(uri, file.name)) + : file.name; + + const isIgnored = ig.ignores(filePath); + + return !isIgnored; + }) + .map(async (file) => { + const name = file.name; + const absoluteUri = path.join(rootPath, name); + if (file.isDirectory()) { + return { + name: name, + isFolder: true, + subDirItems: options.isRecursive + ? await listPathContent(absoluteUri, options, baseUri ?? uri) + : [], + uri: absoluteUri.replace(/\\/g, "/"), + }; + } + + return { + name, + isFolder: false, + uri: absoluteUri.replace(/\\/g, "/"), + }; + }); + + return Promise.all(promise); +} + +// Discover the content of a project +async function handleListPathContent(uri: string, options: any) { + return await listPathContent(uri, options); +} + +async function handleCreateProject(uri: string) { + // Create a folder at the validated path + await fs.promises.mkdir(uri); +} + +async function handleDeleteProject(uri: string) { + // Delete the folder at the validated path + await fs.promises.rm(uri, { recursive: true, force: true }); +} + +async function handleUpdateProject( + uri: string, + updatedInfo: { + name: string; + ctime?: Date; + }, +) { + const newUri = path.join(path.dirname(uri), updatedInfo.name); + await fs.promises.rename(uri, newUri); +} + +async function handleCreateFolder(uri: string) { + // Create a folder at the validated path + await fs.promises.mkdir(uri); +} + +async function handleCreateFile(uri: string) { + // Create a file at the validated path + await fs.promises.writeFile(uri, ""); +} + +async function handleRename(oldUri: string, newUri: string) { + await fs.promises.rename( + oldUri, + newUri, + ); +} + +async function handleDelete(uri: string) { + await fs.promises.rm(uri, { + recursive: true, + force: true, + }); +} + +async function handleHasPath(uri: string) { + return fs.existsSync(uri); +} + +async function handleReadFile(uri: string) { + // Read the file at validated path + const data = await fs.promises.readFile( + uri, + "utf-8", + ); + + return data; +} + +async function handleWriteFile(data: any, uri: string) { + // Write the data at validated path + const safePath = uri; + // create parent directory if it doesn't exist + const dir = path.dirname(safePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(safePath, data); +} + +async function handleCopyFiles(from: string, to: string) { + // Copy the files from the validated from path to the validated to path + await fs.promises.cp( + from, + to, + { + recursive: true, + }, + ); +} + +async function handleLoadSettings() { + if (fs.existsSync(settingsPath)) { + const data = fs.readFileSync(settingsPath, "utf-8"); + return JSON.parse(data); + } + return {}; +} + +async function handleSaveSettings(settings: any) { + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); +} + +async function handleGetInstallationPath() { + const uri = "~/pulse-editor"; + return uri; +} diff --git a/remote-instance/src/servers/node-pty/index.ts b/remote-workspace/src/servers/node-pty/index.ts similarity index 72% rename from remote-instance/src/servers/node-pty/index.ts rename to remote-workspace/src/servers/node-pty/index.ts index 807fcd6e..6810742d 100644 --- a/remote-instance/src/servers/node-pty/index.ts +++ b/remote-workspace/src/servers/node-pty/index.ts @@ -13,6 +13,7 @@ const spawnShell = () => { return spawn(shell, [], { name: "xterm-color", env: process.env, + cwd: "/workspace" }); }; @@ -26,14 +27,20 @@ const setSharedTerminalMode = (useSharedTerminal: boolean) => { const handleTerminalConnection = (ws: WebSocket) => { let ptyProcess = sharedTerminalMode ? sharedPtyProcess : spawnShell(); - ws.on("message", (command: string) => { - const processedCommand = commandProcessor(command); - ptyProcess?.write(processedCommand); + ws.on("message", (data: string) => { + const dataObj = JSON.parse(data); + + if (dataObj.type === "input") { + const command = dataObj.payload; + ptyProcess?.write(command); + } else if (dataObj.type === "resize") { + const { cols, rows } = dataObj.payload; + ptyProcess?.resize(cols, rows); + } }); ptyProcess?.onData((rawOutput) => { - const processedOutput = outputProcessor(rawOutput); - ws.send(processedOutput); + ws.send(JSON.stringify({ type: "output", payload: rawOutput })); }); ws.on("close", () => { @@ -43,20 +50,14 @@ const handleTerminalConnection = (ws: WebSocket) => { }); }; -// Utility function to process commands -const commandProcessor = (command: string) => { - return command; -}; - -// Utility function to process output -const outputProcessor = (output: string) => { - return output; -}; /* Host ws node-pty server */ -setSharedTerminalMode(false); // Set this to false to allow a shared session +setSharedTerminalMode(true); -export function createTerminalServer(server: http.Server | https.Server) { +export function addTerminalServer( + server: http.Server | https.Server, + instanceId: string, +) { const wss = new WebSocketServer({ noServer: true }); wss.on("connection", handleTerminalConnection); @@ -80,8 +81,8 @@ export function createTerminalServer(server: http.Server | https.Server) { return; } - const instanceId = match?.[1]; - if (instanceId !== process.env.INSTANCE_ID) { + const id = match?.[1]; + if (id !== instanceId) { socket.write("HTTP/1.1 400 Bad Request: Invalid instance ID\r\n\r\n"); socket.destroy(); return; diff --git a/remote-instance/tsconfig.json b/remote-workspace/tsconfig.json similarity index 90% rename from remote-instance/tsconfig.json rename to remote-workspace/tsconfig.json index fe93931e..cd690105 100644 --- a/remote-instance/tsconfig.json +++ b/remote-workspace/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "module": "node20", + "module": "nodenext", "moduleResolution": "nodenext", "outDir": "dist", "rootDir": "src", diff --git a/remote-instance/utils/generate-self-signed.ps1 b/remote-workspace/utils/generate-self-signed.ps1 similarity index 100% rename from remote-instance/utils/generate-self-signed.ps1 rename to remote-workspace/utils/generate-self-signed.ps1 diff --git a/remote-instance/utils/generate-self-signed.sh b/remote-workspace/utils/generate-self-signed.sh similarity index 100% rename from remote-instance/utils/generate-self-signed.sh rename to remote-workspace/utils/generate-self-signed.sh diff --git a/web/README.md b/web/README.md index d788fa9b..ad78fd69 100644 --- a/web/README.md +++ b/web/README.md @@ -1,12 +1,66 @@ +# Generate certificates for local dev + +## Do the below in `web/certificates` folder + +1. Generate a local CA. + Use devCA.key to install on device later. + +```bash +openssl genrsa -out devCA.key 2048 +openssl req -x509 -new -nodes -key devCA.key -sha256 -days 3650 -out devCA.crt -subj "/CN=Local Development CA" +``` + +2. Create a .cnf + e.g. + +```bash +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext + +[dn] +CN = 192.168.1.100 + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +IP.1 = 192.168.1.100 +DNS.1 = mypc +DNS.2 = mypc.local +DNS.3 = localhost +``` + +3. Generate private key + +```bash +openssl genrsa -out localhost-key.pem 2048 +openssl req -new -key localhost-key.pem -out localhost.csr -config localhost.cnf +``` + +4. Sign the certificate with CA + +```bash +openssl x509 -req -in localhost.csr -CA devCA.crt -CAkey devCA.key -CAcreateserial -out localhost.pem -days 365 -sha256 -extensions req_ext -extfile localhost.cnf +``` + +5. Install `devCA.crt` on Android. + First, copy `devCA.crt` from PC to you android device. On Android, go to "settings -> security and privacy -> more security settings -> install from device storage -> CA certificate", then locate `devCA.crt` and install. + # Web Client User Guide + ### Installation -You must then configure settings in the app. Specifically, to use Voice Chat, you need to have all STT, LLM, TTS configured; to only use Agentic Chat Terminal or Code Completion, you need to configure LLM. + +You must then configure settings in the app. Specifically, to use Voice Chat, you need to have all STT, LLM, TTS configured; to only use Agentic Chat Terminal or Code Completion, you need to configure LLM. | Modality | Supported Provider | -| --- | --- | -| STT | OpenAI | -| LLM | OpenAI | -| TTS | OpenAI, ElevenLabs | +| -------- | ------------------ | +| STT | OpenAI | +| LLM | OpenAI | +| TTS | OpenAI, ElevenLabs | (For TTS, you need to enter a voice name or voice ID which you can find from your provider. e.g. “alloy” for OpenAI TTS1, “Maltida” for ElevenLabs.) @@ -34,4 +88,4 @@ Click the “Open Chat View” icon in the bottom toolbar. Then select your desi (Make sure you have configured LLM provider and API key) -Type anything in an open file, then a suggestion would become available in grey text. Press tab key to accept changes, or keep typing to refresh new suggest. \ No newline at end of file +Type anything in an open file, then a suggestion would become available in grey text. Press tab key to accept changes, or keep typing to refresh new suggest. diff --git a/web/app/(main-layout)/layout.tsx b/web/app/(main-layout)/layout.tsx index 48165913..f0c5e57f 100644 --- a/web/app/(main-layout)/layout.tsx +++ b/web/app/(main-layout)/layout.tsx @@ -1,16 +1,16 @@ -import type { Metadata } from "next"; -import "./globals.css"; -import WrappedHeroUIProvider from "@/components/providers/wrapped-hero-ui-provider"; -import EditorContextProvider from "@/components/providers/editor-context-provider"; -import { Toaster } from "react-hot-toast"; -import "material-icons/iconfont/material-icons.css"; +import Nav from "@/components/interface/navigation/nav"; import CapacitorProvider from "@/components/providers/capacitor-provider"; -import RemoteModuleProvider from "@/components/providers/remote-module-provider"; +import EditorContextProvider from "@/components/providers/editor-context-provider"; import InterModuleCommunicationProvider from "@/components/providers/imc-provider"; -import Nav from "@/components/interface/navigation/nav"; -import { Suspense } from "react"; -import { Analytics } from "@vercel/analytics/next"; import PlatformAssistantProvider from "@/components/providers/platform-assistant-provider"; +import RemoteModuleProvider from "@/components/providers/remote-module-provider"; +import WrappedHeroUIProvider from "@/components/providers/wrapped-hero-ui-provider"; +import { Analytics } from "@vercel/analytics/next"; +import "material-icons/iconfont/material-icons.css"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { Toaster } from "react-hot-toast"; +import "./globals.css"; export const metadata: Metadata = { title: "Pulse Editor", @@ -27,9 +27,9 @@ export default function RootLayout({ - - - + + + @@ -40,9 +40,9 @@ export default function RootLayout({ - - - + + + diff --git a/web/components/explorer/file-system/fs-explorer.tsx b/web/components/explorer/file-system/fs-explorer.tsx index 3703c721..506bc138 100644 --- a/web/components/explorer/file-system/fs-explorer.tsx +++ b/web/components/explorer/file-system/fs-explorer.tsx @@ -24,10 +24,17 @@ export default function FileSystemExplorer({ const platform = getPlatform(); const { platformApi } = usePlatformApi(); - const { activeTabView, closeAllTabViews } = useTabViewManager(); + const { activeTabView } = useTabViewManager(); const rootGroupRef = useRef(null); + const content = editorContext?.editorStates.workspaceContent ?? []; + + const fsPath = + editorContext?.persistSettings?.projectHomePath + + "/" + + editorContext?.editorStates.project; + // Reset root group ref when there are other nodes selected useEffect(() => { const selectedNodes = @@ -172,22 +179,6 @@ export default function FileSystemExplorer({
- {/* @@ -200,24 +191,18 @@ export default function FileSystemExplorer({
- {editorContext?.editorStates.projectContent?.length === 0 && ( -
-

- Empty content. Create a new file to get started. -

-
+ {content?.length === 0 && ( +

+ Empty content. Create a new file to get started. +

)}
diff --git a/web/components/explorer/file-system/tree-view.tsx b/web/components/explorer/file-system/tree-view.tsx index 57580360..551789cf 100644 --- a/web/components/explorer/file-system/tree-view.tsx +++ b/web/components/explorer/file-system/tree-view.tsx @@ -4,11 +4,11 @@ import ContextMenu from "@/components/interface/context-menu"; import Icon from "@/components/misc/icon"; import { EditorContext } from "@/components/providers/editor-context-provider"; import { DragEventTypeEnum, PlatformEnum } from "@/lib/enums"; +import { useWorkspace } from "@/lib/hooks/use-workspace"; import { AbstractPlatformAPI } from "@/lib/platform-api/abstract-platform-api"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { ContextMenuState, - EditorContextType, FileDragData, FileSystemObject, TreeViewGroupRef, @@ -27,34 +27,6 @@ import { } from "react"; import toast from "react-hot-toast"; -function refreshProjectContent( - platformApi: AbstractPlatformAPI, - editorContext: EditorContextType | undefined, -) { - const projectUri = - editorContext?.persistSettings?.projectHomePath + - "/" + - editorContext?.editorStates.project; - platformApi - ?.listPathContent(projectUri, { - include: "all", - isRecursive: true, - }) - .then((objects) => { - editorContext?.setEditorStates((prev) => { - return { - ...prev, - projectContent: objects, - explorerSelectedNodeRefs: [], - }; - }); - - console.log("Found project content:", objects); - - toast.success("Project content updated."); - }); -} - // A tree view node that represents a single file or folder const TreeViewNode = forwardRef(function TreeViewNode( { @@ -82,6 +54,8 @@ const TreeViewNode = forwardRef(function TreeViewNode( }, })); + const { refreshWorkspaceContent } = useWorkspace(); + const [isFolderCollapsed, setIsFolderCollapsed] = useState(true); const [isSelected, setIsSelected] = useState(false); const editorContext = useContext(EditorContext); @@ -218,7 +192,7 @@ const TreeViewNode = forwardRef(function TreeViewNode( parentGroupRef.current?.getFolderUri() + "/" + newName; platformApi?.rename(object.uri, newUri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setIsRenaming(false); @@ -230,7 +204,7 @@ const TreeViewNode = forwardRef(function TreeViewNode( parentGroupRef.current?.getFolderUri() + "/" + newName; platformApi?.rename(object.uri, newUri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setIsRenaming(false); @@ -318,35 +292,11 @@ const TreeViewNode = forwardRef(function TreeViewNode( )} -
- - +
{!object.isFolder && ( )} + +
@@ -452,12 +424,13 @@ const TreeViewGroup = forwardRef(function TreeViewGroup( }, })); + const { refreshWorkspaceContent } = useWorkspace(); + const [isCreatingNewFile, setIsCreatingNewFile] = useState(false); const [isCreatingNewFolder, setIsCreatingNewFolder] = useState(false); const [folderNameInputValue, setFolderNameInputValue] = useState(""); const [fileNameInputValue, setFileNameInputValue] = useState(""); - const editorContext = useContext(EditorContext); function createNewFolder(uri: string) { console.log("Creating new folder with uri:", uri); @@ -468,7 +441,7 @@ const TreeViewGroup = forwardRef(function TreeViewGroup( } platformApi.createFolder(uri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setFolderNameInputValue(""); @@ -484,7 +457,7 @@ const TreeViewGroup = forwardRef(function TreeViewGroup( } platformApi.createFile(uri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setFileNameInputValue(""); diff --git a/web/components/explorer/project/project-explorer.tsx b/web/components/explorer/project/project-explorer.tsx index 272f8214..ae14b001 100644 --- a/web/components/explorer/project/project-explorer.tsx +++ b/web/components/explorer/project/project-explorer.tsx @@ -1,7 +1,9 @@ "use client"; +import { SideMenuTabEnum } from "@/lib/enums"; import { usePlatformApi } from "@/lib/hooks/use-platform-api"; import { ProjectInfo } from "@/lib/types"; +import { Button } from "@heroui/react"; import { useContext, useEffect, useState } from "react"; import ProjectSettingsModal from "../../modals/project-settings-modal"; import { EditorContext } from "../../providers/editor-context-provider"; @@ -17,6 +19,12 @@ export default function ProjectExplorer() { ProjectInfo | undefined >(undefined); + useEffect(() => { + if (editorContext?.editorStates.project) { + // Get workflows stored either on cloud or locally in project/.workflows + } + }, [editorContext?.editorStates.project]); + useEffect(() => { if (platformApi) { const homePath = editorContext?.persistSettings?.projectHomePath; @@ -34,19 +42,36 @@ export default function ProjectExplorer() { return (
- {editorContext?.editorStates.projectsInfo?.map((project, index) => ( - +

View Projects

+ + {editorContext?.editorStates.projectsInfo?.map((project, index) => ( + { + editorContext.setEditorStates((prev) => ({ + ...prev, + sideMenuTab: SideMenuTabEnum.Apps, + })); + }} + /> + ))} + - ))} - +
); } diff --git a/web/components/explorer/project/project-item.tsx b/web/components/explorer/project/project-item.tsx index b732f8d2..f565c15d 100644 --- a/web/components/explorer/project/project-item.tsx +++ b/web/components/explorer/project/project-item.tsx @@ -1,6 +1,4 @@ -import { PlatformEnum, SideMenuTabEnum } from "@/lib/enums"; import { usePlatformApi } from "@/lib/hooks/use-platform-api"; -import { getPlatform, isWeb } from "@/lib/platform-api/platform-checker"; import { ContextMenuState, ProjectInfo } from "@/lib/types"; import { Button } from "@heroui/react"; import { useContext, useState } from "react"; @@ -11,10 +9,12 @@ export default function ProjectItem({ project, setSettingsOpen, setSettingsProject, + onOpen, }: { project: ProjectInfo; setSettingsOpen: (isOpen: boolean) => void; setSettingsProject: (project: ProjectInfo) => void; + onOpen?: () => void; }) { const editorContext = useContext(EditorContext); @@ -38,27 +38,6 @@ export default function ProjectItem({ project: projectName, }; }); - - if (getPlatform() === PlatformEnum.Electron) { - const uri = - editorContext?.persistSettings?.projectHomePath + "/" + projectName; - platformApi - ?.listPathContent(uri, { - include: "all", - isRecursive: true, - }) - .then((objects) => { - editorContext?.setEditorStates((prev) => { - return { - ...prev, - project: projectName, - projectContent: objects, - }; - }); - }); - } else if (isWeb()) { - // TODO: move this to when workspace is loaded - } } function formatDateTime(date: Date) { @@ -80,6 +59,9 @@ export default function ProjectItem({ // Only open project if context menu is not open if (!contextMenuState.isOpen) { openProject(projectName); + if (onOpen) { + onOpen(); + } } }} onContextMenu={(e) => { diff --git a/web/components/explorer/workspace/workspace-explorer.tsx b/web/components/explorer/workspace/workspace-explorer.tsx index bd2f35a8..b60e47d0 100644 --- a/web/components/explorer/workspace/workspace-explorer.tsx +++ b/web/components/explorer/workspace/workspace-explorer.tsx @@ -1,5 +1,176 @@ -import WIP from "@/components/interface/status-screens/wip"; +import Icon from "@/components/misc/icon"; +import WorkspaceSettingsModal from "@/components/modals/workspace-settings-model"; +import { EditorContext } from "@/components/providers/editor-context-provider"; +import { PlatformEnum } from "@/lib/enums"; +import { usePlatformApi } from "@/lib/hooks/use-platform-api"; +import { useWorkspace } from "@/lib/hooks/use-workspace"; +import { getPlatform } from "@/lib/platform-api/platform-checker"; +import { Select, SelectItem } from "@heroui/react"; +import { useContext, useEffect, useState } from "react"; +import FileSystemExplorer from "../file-system/fs-explorer"; export default function WorkspaceExplorer() { - return ; + const editorContext = useContext(EditorContext); + + const workspaceHook = useWorkspace(); + const { platformApi } = usePlatformApi(); + + const [isWorkspaceSettingsModalOpen, setIsWorkspaceSettingsModalOpen] = + useState(false); + + const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); + + useEffect(() => { + async function openProjectInWorkspace() { + if (!platformApi) { + return; + } + + if (getPlatform() === PlatformEnum.Electron && !workspaceHook.workspace) { + await workspaceHook.refreshWorkspaceContent(platformApi); + } else { + if (isCreatingWorkspace && workspaceHook.workspace) { + const homePath = editorContext?.persistSettings?.projectHomePath; + const projectName = editorContext?.editorStates.project; + if (!projectName) { + return; + } + + const uri = homePath + "/" + projectName; + const hasPath = await platformApi.hasPath(uri); + + if (!hasPath) { + await platformApi.createFolder(uri); + } + + await workspaceHook.refreshWorkspaceContent(platformApi); + setIsCreatingWorkspace(false); + } + } + } + + openProjectInWorkspace(); + }, [platformApi]); + + return ( +
+ {editorContext?.editorStates.project ? ( +
+
+ +
+ {getPlatform() === PlatformEnum.Electron || + workspaceHook.workspace ? ( + { + editorContext?.setEditorStates((prev) => ({ + ...prev, + isSideMenuOpen: false, + })); + }} + /> + ) : ( +
+

+ To browse files in workspace, please open in desktop client or + select remote workspace. +

+
+ )} +
+ ) : ( +
+

+ To view project content in workspace, please select a project first. +

+
+ )} + + {isWorkspaceSettingsModalOpen && ( + + )} +
+ ); } diff --git a/web/components/interface/navigation/nav-side-menu.tsx b/web/components/interface/navigation/nav-side-menu.tsx index d0cbe378..797e2487 100644 --- a/web/components/interface/navigation/nav-side-menu.tsx +++ b/web/components/interface/navigation/nav-side-menu.tsx @@ -1,18 +1,16 @@ import AppExplorer from "@/components/explorer/app/app-explorer"; -import FileSystemExplorer from "@/components/explorer/file-system/fs-explorer"; import ProjectExplorer from "@/components/explorer/project/project-explorer"; import WorkspaceExplorer from "@/components/explorer/workspace/workspace-explorer"; import Tabs from "@/components/misc/tabs"; -import ProjectSettingsModal from "@/components/modals/project-settings-modal"; import { EditorContext } from "@/components/providers/editor-context-provider"; -import { SideMenuTabEnum } from "@/lib/enums"; +import { PlatformEnum, SideMenuTabEnum } from "@/lib/enums"; import useExplorer from "@/lib/hooks/use-explorer"; import { useScreenSize } from "@/lib/hooks/use-screen-size"; -import { isWeb } from "@/lib/platform-api/platform-checker"; +import { getPlatform } from "@/lib/platform-api/platform-checker"; import { TabItem } from "@/lib/types"; import { Button } from "@heroui/react"; import { AnimatePresence, motion } from "framer-motion"; -import { useContext, useState } from "react"; +import { useContext } from "react"; import Icon from "../../misc/icon"; export default function NavSideMenu({ @@ -26,7 +24,7 @@ export default function NavSideMenu({ {isMenuOpen && ( -
+
- +
@@ -102,18 +100,11 @@ function MenuPanel({ children }: { children?: React.ReactNode }) { ); } -function PanelContent({ - setIsMenuOpen, -}: { - setIsMenuOpen: (isOpen: boolean) => void; -}) { +function PanelContent() { const editorContext = useContext(EditorContext); const { selectAndSetProjectHome } = useExplorer(); - const [isProjectSettingsModalOpen, setIsProjectSettingsModalOpen] = - useState(false); - const tabItems: TabItem[] = [ { name: SideMenuTabEnum.Projects, @@ -126,7 +117,7 @@ function PanelContent({ icon: "apps", }, { - name: SideMenuTabEnum.Workspaces, + name: SideMenuTabEnum.Workspace, description: "Project workspace", icon: "folder", }, @@ -144,7 +135,10 @@ function PanelContent({ } // Choose project home path - if (!isWeb() && !editorContext?.persistSettings?.projectHomePath) { + if ( + getPlatform() === PlatformEnum.Electron && + !editorContext?.persistSettings?.projectHomePath + ) { return (

@@ -164,7 +158,7 @@ function PanelContent({ } return ( -

+
{selectedTab === SideMenuTabEnum.Apps ? ( - ) : selectedTab === SideMenuTabEnum.Workspaces ? ( - // + ) : selectedTab === SideMenuTabEnum.Workspace ? ( ) : ( - selectedTab === SideMenuTabEnum.Projects && - (editorContext?.editorStates.project ? ( - - ) : ( + selectedTab === SideMenuTabEnum.Projects && (
-

View Projects

-
- )) + ) )}
-
); } diff --git a/web/components/interface/navigation/nav-top-bar.tsx b/web/components/interface/navigation/nav-top-bar.tsx index 3c05959e..6a0d9430 100644 --- a/web/components/interface/navigation/nav-top-bar.tsx +++ b/web/components/interface/navigation/nav-top-bar.tsx @@ -1,16 +1,11 @@ -import { PlatformEnum } from "@/lib/enums"; -import { useAuth } from "@/lib/hooks/use-auth"; import { useMenuActions } from "@/lib/hooks/menu-actions/use-menu-actions"; -import { useWorkspace } from "@/lib/hooks/use-workspace"; -import { getPlatform } from "@/lib/platform-api/platform-checker"; +import { useAuth } from "@/lib/hooks/use-auth"; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, - Select, - SelectItem, } from "@heroui/react"; import { useTheme } from "next-themes"; import { useSearchParams } from "next/navigation"; @@ -25,20 +20,16 @@ import ViewMenuDropDown from "./menu-dropdown/view-menu"; export default function NavTopBar({ isMenuOpen, setIsMenuOpen, - setIsWorkspaceSettingsModalOpen, setIsSharingOpen, }: { isMenuOpen: boolean; setIsMenuOpen: (isOpen: boolean) => void; - setIsWorkspaceSettingsModalOpen: (isOpen: boolean) => void; setIsSharingOpen: (isOpen: boolean) => void; }) { const editorContext = useContext(EditorContext); const { session, signOut } = useAuth(); - const { theme, setTheme } = useTheme(); - - const workspaceHook = useWorkspace(); + const { resolvedTheme, setTheme } = useTheme(); // #region Load specified app if app query parameter is present const params = useSearchParams(); @@ -82,71 +73,6 @@ export default function NavTopBar({ - - {/* Do not show workspace selector when the app is open in web, and session is not available */} - {(getPlatform() === PlatformEnum.Web || - getPlatform() === PlatformEnum.WebMobile) && - !session ? null : ( - - )}
{editorContext?.editorStates.project && } @@ -173,6 +99,21 @@ export default function NavTopBar({ + {!session && ( )} - {session && ( diff --git a/web/components/interface/navigation/nav.tsx b/web/components/interface/navigation/nav.tsx index cf8a87a0..f65faefb 100644 --- a/web/components/interface/navigation/nav.tsx +++ b/web/components/interface/navigation/nav.tsx @@ -2,18 +2,14 @@ import { PlatformEnum } from "@/lib/enums"; import { useAuth } from "@/lib/hooks/use-auth"; -import { useWorkspace } from "@/lib/hooks/use-workspace"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { SafeArea } from "@capacitor-community/safe-area"; -import { addToast, Button } from "@heroui/react"; -import { PulseEditorCapacitor } from "@pulse-editor/capacitor-plugin"; import { useTheme } from "next-themes"; import { useContext, useEffect, useState } from "react"; import AppInfoModal from "../../modals/app-info-modal"; import LoginModal from "../../modals/login-modal"; import PasswordModal from "../../modals/password-modal"; import SharingModal from "../../modals/sharing-modal"; -import WorkspaceSettingsModal from "../../modals/workspace-settings-model"; import { EditorContext } from "../../providers/editor-context-provider"; import Loading from "../status-screens/loading"; import NavSideMenu from "./nav-side-menu"; @@ -27,63 +23,11 @@ export default function Nav({ children }: { children: React.ReactNode }) { const { setTheme, resolvedTheme } = useTheme(); const { session, isLoading: isLoadingSession, signIn } = useAuth(); - const workspaceHook = useWorkspace(); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); const [isShowNavbar, setIsShowNavbar] = useState(true); - const [isWorkspaceSettingsModalOpen, setIsWorkspaceSettingsModalOpen] = - useState(false); - const [isSharingOpen, setIsSharingOpen] = useState(false); - - const [isGranted, setIsGranted] = useState(true); - - useEffect(() => { - async function checkStoragePermission() { - const { isGranted }: { isGranted: boolean } = - await PulseEditorCapacitor.isManageStoragePermissionGranted(); - setIsGranted(isGranted); - } - - if (getPlatform() === PlatformEnum.Capacitor) { - checkStoragePermission(); - } - }, []); - useEffect(() => { - if (!isGranted) { - addToast({ - title: "Storage Permission Required", - classNames: { - base: "flex flex-col items-start", - }, - description: - "To use local storage management feature, please grant storage permission in settings. ", - icon: "warning", - color: "warning", - size: "lg", - shouldShowTimeoutProgress: true, - timeout: 30000, - endContent: ( -
- - -
- ), - }); - } - }, [isGranted]); + const [isSharingOpen, setIsSharingOpen] = useState(false); useEffect(() => { const platform = getPlatform(); @@ -155,14 +99,6 @@ export default function Nav({ children }: { children: React.ReactNode }) { )} - {isWorkspaceSettingsModalOpen && ( - - )} - {isSharingOpen && ( )} @@ -183,7 +119,6 @@ export default function Nav({ children }: { children: React.ReactNode }) { )} diff --git a/web/components/interface/project-indicator.tsx b/web/components/interface/project-indicator.tsx index 764d5d7f..e0679235 100644 --- a/web/components/interface/project-indicator.tsx +++ b/web/components/interface/project-indicator.tsx @@ -1,7 +1,6 @@ "use client"; -import { Key, useContext, useState } from "react"; -import { EditorContext } from "../providers/editor-context-provider"; +import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager"; import { Button, Dropdown, @@ -9,9 +8,10 @@ import { DropdownMenu, DropdownTrigger, } from "@heroui/react"; +import { Key, useContext, useState } from "react"; import Icon from "../misc/icon"; import ProjectSettingsModal from "../modals/project-settings-modal"; -import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager"; +import { EditorContext } from "../providers/editor-context-provider"; export default function ProjectIndicator() { const editorContext = useContext(EditorContext); @@ -24,7 +24,7 @@ export default function ProjectIndicator() { return { ...prev, project: "", - projectContent: [], + workspaceContent: [], }; }); diff --git a/web/components/interface/subscription/plan-picker.tsx b/web/components/interface/subscription/plan-picker.tsx new file mode 100644 index 00000000..34b84949 --- /dev/null +++ b/web/components/interface/subscription/plan-picker.tsx @@ -0,0 +1,17 @@ +import { Button } from "@heroui/react"; + +export default function PlanPicker() { + return ( +
+
+ Free: Use free plan and enjoy basic features. Access to self-hosted + workspaces and bring your own API keys. This option will remain + available forever and we are committed to keeping it free and open + source. + +
+
+ ); +} diff --git a/web/components/marketplace/app/app-gallery.tsx b/web/components/marketplace/app/app-gallery.tsx index 5f88d201..969be061 100644 --- a/web/components/marketplace/app/app-gallery.tsx +++ b/web/components/marketplace/app/app-gallery.tsx @@ -35,7 +35,7 @@ export default function AppGallery() { } = useSWR( selectLabels[selectedIndex]?.name === "All" || selectLabels[selectedIndex]?.name === "Published by Me" - ? `/api/extension/list${selectLabels[selectedIndex].name === "Published by Me" ? "?published=true" : ""}` + ? `/api/app/list${selectLabels[selectedIndex].name === "Published by Me" ? "?published=true" : ""}` : null, async (url: string) => { const res = await fetchAPI(url); @@ -107,7 +107,7 @@ export default function AppGallery() { }, extGroup[0]); return ( -
+
+
- Coming soon + + {workspace ? ( - +
+ + + +
) : ( )} diff --git a/web/components/providers/capacitor-provider.tsx b/web/components/providers/capacitor-provider.tsx index fc70ecb0..7d5f1003 100644 --- a/web/components/providers/capacitor-provider.tsx +++ b/web/components/providers/capacitor-provider.tsx @@ -1,15 +1,21 @@ "use client"; -import { useEffect } from "react"; -import { StatusBar } from "@capacitor/status-bar"; -import { ScreenOrientation } from "@capacitor/screen-orientation"; +import { App, URLOpenListenerEvent } from "@capacitor/app"; import { Capacitor } from "@capacitor/core"; +import { Preferences } from "@capacitor/preferences"; +import { ScreenOrientation } from "@capacitor/screen-orientation"; +import { StatusBar } from "@capacitor/status-bar"; +import { useContext, useEffect } from "react"; +import { EditorContext } from "./editor-context-provider"; export default function CapacitorProvider({ children, }: { children: React.ReactNode; }) { + const editorContext = useContext(EditorContext); + + // Set status bar based on orientation useEffect(() => { async function setStatusBar() { const orientation = await ScreenOrientation.orientation(); @@ -31,5 +37,62 @@ export default function CapacitorProvider({ ScreenOrientation.addListener("screenOrientationChange", setStatusBar); } }, []); + + // Set deep linking listener + useEffect(() => { + App.addListener("appUrlOpen", async (event: URLOpenListenerEvent) => { + /* + Custom Scheme + */ + // check if the url has our custom scheme "pulse-editor://open" + if (event.url.startsWith("pulse-editor://open")) { + const params = new URLSearchParams( + event.url.replace("pulse-editor://open?", ""), + ); + const token = params.get("token"); + const exp = params.get("exp"); + + // Set token in Preferences and refresh session. + // After refresh, the cookie will be set in the webview automatically. + // Hence, no need to set cookie manually here. + if (token) { + await Preferences.set({ + key: "pulse-editor.session-token", + value: token, + }); + if (exp) { + await Preferences.set({ + key: "pulse-editor.session-expiration", + value: exp, + }); + } + } else { + await Preferences.remove({ + key: "pulse-editor.session-token", + }); + } + editorContext?.setEditorStates((prev) => ({ + ...prev, + isRefreshSession: true, + isSigningIn: false, + })); + } + /* + Google Verified Links + Check if the url has our scheme "https://mobile.pulse-editor.com" + */ + // else if (event.url.startsWith("https://pulse-editor.com/mobile")) { + // const slug = event.url.replace("https://pulse-editor.com/mobile", ""); + // // Navigate to the slug + // if (slug) { + // router.push(slug); + // } + // } + + // If no match, do nothing - let regular routing + // logic take over + }); + }, []); + return
{children}
; } diff --git a/web/components/providers/imc-provider.tsx b/web/components/providers/imc-provider.tsx index ae4d92fc..937fe583 100644 --- a/web/components/providers/imc-provider.tsx +++ b/web/components/providers/imc-provider.tsx @@ -169,7 +169,7 @@ export default function InterModuleCommunicationProvider({ throw new Error("Agent method not found."); } - if (editorContext?.persistSettings?.isUseManagedCloud) { + if (editorContext?.persistSettings?.isUseManagedCloud ?? true) { const result = await runAgentMethodCloud(agent, methodName, args); return result; diff --git a/web/components/tools/voice.tsx b/web/components/tools/voice.tsx index 6e79d0ae..eaa64254 100644 --- a/web/components/tools/voice.tsx +++ b/web/components/tools/voice.tsx @@ -63,7 +63,7 @@ export default function Voice({ } } - if (isUseManagedCloud) { + if (isUseManagedCloud ?? true) { return ( ( + const { + data: session, + isLoading, + mutate, + } = useSWR( !editorContext?.editorStates.isSigningIn ? `/api/auth/session` : null, async (url: string) => { const res = await fetchAPI(url); @@ -53,6 +60,41 @@ export function useAuth() { }, ); + useEffect(() => { + async function refreshSession() { + if (editorContext?.editorStates.isRefreshSession) { + const token = await Preferences.get({ + key: "pulse-editor.session-token", + }); + + /* + Sometimes other hooks using useSWR are fired right after retuning from deep linking + before session is refreshed (triggered by window re-focus), causing cookies to be + set again between when it is removed (if removed in deep link handler) and when + session is refreshed. + + So a better approach is to clear cookies right here before refreshing session, but + possibly after other hooks are fired. + */ + if (!token.value) { + // CapacitorCookies.clearAllCookies(); + CapacitorCookies.deleteCookie({ + key: "pulse-editor.session-token", + url: process.env.NEXT_PUBLIC_BACKEND_URL, + }); + } + + await mutate(); + editorContext.setEditorStates((prev) => ({ + ...prev, + isRefreshSession: false, + })); + } + } + + refreshSession(); + }, [editorContext?.editorStates.isRefreshSession]); + // Open a sign-in page if the user is not signed in. async function signIn() { if (session) { @@ -64,6 +106,16 @@ export function useAuth() { // TODO: move this to the platform API layer // @ts-expect-error window.electronAPI is exposed by the Electron main process window.electronAPI.login(); + } else if (getPlatform() === PlatformEnum.Capacitor) { + // In Capacitor, open the sign-in page in the system browser. + const url = getAPIUrl(`/api/auth/signin`); + // Set the callback URL to the deeplink URL that Capacitor can handle. + url.searchParams.set( + "callbackUrl", + process.env.NEXT_PUBLIC_BACKEND_URL + "/api/mobile", + ); + + await Browser.open({ url: url.toString() }); } else { const url = getAPIUrl(`/api/auth/signin`); url.searchParams.set("callbackUrl", window.location.href); @@ -82,6 +134,15 @@ export function useAuth() { // TODO: move this to the platform API layer // @ts-expect-error window.electronAPI is exposed by the Electron main process window.electronAPI.logout(); + } else if (getPlatform() === PlatformEnum.Capacitor) { + // In Capacitor, open the sign-out page in the system browser. + const url = getAPIUrl(`/api/auth/signout`); + // Set the callback URL to the deeplink URL that Capacitor can handle. + url.searchParams.set( + "callbackUrl", + process.env.NEXT_PUBLIC_BACKEND_URL + "/api/mobile", + ); + await Browser.open({ url: url.toString() }); } else { const url = getAPIUrl(`/api/auth/signout`); url.searchParams.set("callbackUrl", window.location.href); diff --git a/web/lib/hooks/use-extension-manager.ts b/web/lib/hooks/use-extension-manager.ts index 19b1398c..7bf9dbce 100644 --- a/web/lib/hooks/use-extension-manager.ts +++ b/web/lib/hooks/use-extension-manager.ts @@ -262,7 +262,7 @@ export default function useExtensionManager() { // Download and load the extension app if specified async function loadAppFromRegistry(appId: string, inviteCode?: string) { - const url = getAPIUrl(`/api/extension/get`); + const url = getAPIUrl(`/api/app/get`); url.searchParams.set("name", appId); url.searchParams.set("latest", "true"); if (inviteCode) url.searchParams.set("inviteCode", inviteCode); diff --git a/web/lib/hooks/use-platform-ai-assistant.ts b/web/lib/hooks/use-platform-ai-assistant.ts index 7ea67099..65777a37 100644 --- a/web/lib/hooks/use-platform-ai-assistant.ts +++ b/web/lib/hooks/use-platform-ai-assistant.ts @@ -133,7 +133,7 @@ export default function usePlatformAIAssistant() { const previousMessage = history[history.length - 1].message.content.text; - if (isUseManagedCloud) { + if (isUseManagedCloud ?? true) { const { analysis }: { analysis: string } = await runAgentMethodCloud( editorAssistantAgent, "analyzeCommandResult", @@ -444,7 +444,7 @@ export default function usePlatformAIAssistant() { input: ReadableStream | string | undefined, isOutputAudio: boolean, ) { - if (isUseManagedCloud) { + if (isUseManagedCloud ?? true) { runCloudAssistant(input, isOutputAudio); } else { runLocalAssistant(input, isOutputAudio); diff --git a/web/lib/hooks/use-platform-api.ts b/web/lib/hooks/use-platform-api.ts index 7b1dd010..29bdc5d7 100644 --- a/web/lib/hooks/use-platform-api.ts +++ b/web/lib/hooks/use-platform-api.ts @@ -1,14 +1,15 @@ -import { EditorContext } from "@/components/providers/editor-context-provider"; import { PlatformEnum } from "@/lib/enums"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { AbstractPlatformAPI } from "../platform-api/abstract-platform-api"; import { CapacitorAPI } from "../platform-api/capacitor/capacitor-api"; import { CloudAPI } from "../platform-api/cloud/cloud-api"; import { ElectronAPI } from "../platform-api/electron/electron-api"; import { getPlatform } from "../platform-api/platform-checker"; +import { useWorkspace } from "./use-workspace"; export function usePlatformApi() { - const editorContext = useContext(EditorContext); + const { workspace } = useWorkspace(); + const [platformApi, setPlatformApi] = useState< AbstractPlatformAPI | undefined >(undefined); @@ -20,11 +21,11 @@ export function usePlatformApi() { // When workspace changes, reset platform API if needed useEffect(() => { - if (platformApi && editorContext?.editorStates.currentWorkspace) { + if (platformApi && workspace) { const api = getAbstractPlatformAPI(); setPlatformApi(api); } - }, [editorContext?.editorStates.currentWorkspace]); + }, [workspace]); function getAbstractPlatformAPI(): AbstractPlatformAPI { const platform = getPlatform(); @@ -37,7 +38,6 @@ export function usePlatformApi() { platform === PlatformEnum.Web || platform === PlatformEnum.WebMobile ) { - const workspace = editorContext?.editorStates.currentWorkspace; return new CloudAPI(workspace); } else if (platform === PlatformEnum.VSCode) { // platformApi.current = new VSCodeAPI(); diff --git a/web/lib/hooks/use-workspace.ts b/web/lib/hooks/use-workspace.ts index 9df911f2..f197f781 100644 --- a/web/lib/hooks/use-workspace.ts +++ b/web/lib/hooks/use-workspace.ts @@ -1,44 +1,52 @@ import { EditorContext } from "@/components/providers/editor-context-provider"; import { PlatformEnum } from "@/lib/enums"; -import { useContext, useEffect, useState } from "react"; +import { useContext } from "react"; import useSWR from "swr"; +import { AbstractPlatformAPI } from "../platform-api/abstract-platform-api"; import { getPlatform } from "../platform-api/platform-checker"; -import { fetchAPI, getAPIUrl } from "../pulse-editor-website/backend"; +import { fetchAPI } from "../pulse-editor-website/backend"; import { RemoteWorkspace } from "../types"; import { useAuth } from "./use-auth"; export function useWorkspace() { const editorContext = useContext(EditorContext); - const [workspace, setWorkspace] = useState( - undefined, - ); const { session } = useAuth(); - const { data: cloudWorkspaces } = useSWR( - session ? `/api/workspace/list` : null, - async (url: string) => { - const res = await fetchAPI(url); - if (!res.ok) { - throw new Error("Failed to fetch workspace data"); - } - const { - workspaces, - }: { - workspaces: RemoteWorkspace[]; - } = await res.json(); - - return workspaces; - }, - ); - - // Update workspace state when the editor context changes - useEffect(() => { - if (editorContext?.editorStates?.currentWorkspace) { - setWorkspace(editorContext.editorStates.currentWorkspace); + const { data: cloudWorkspaces, mutate: mutateCloudWorkspaces } = useSWR< + RemoteWorkspace[] + >(session ? `/api/workspace/list` : null, async (url: string) => { + const res = await fetchAPI(url); + if (!res.ok) { + throw new Error("Failed to fetch workspace data"); } - }, [editorContext?.editorStates?.currentWorkspace]); + const { + workspaces, + }: { + workspaces: RemoteWorkspace[]; + } = await res.json(); + + return workspaces; + }); + + const workspace = editorContext?.editorStates?.currentWorkspace; + const setWorkspace = (ws: RemoteWorkspace | undefined) => { + if (!editorContext) { + throw new Error("Editor context is not available"); + } + editorContext.setEditorStates((prev) => { + return { + ...prev, + currentWorkspace: ws, + }; + }); + }; - async function createWorkspace(name: string) { + async function createWorkspace( + name: string, + cpuLimit: string, + memoryLimit: string, + volumeSize: string, + ) { if (!editorContext) { throw new Error("Editor context is not available"); } else if ( @@ -53,50 +61,60 @@ export function useWorkspace() { } // Request to create a new workspace - const response = await fetchAPI(`/api/workspace/create`); + const response = await fetchAPI(`/api/workspace/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, cpuLimit, memoryLimit, volumeSize }), + }); + + if (!response.ok) { + throw new Error(await response.text()); + } const { id, - createdAt, - updatedAt, }: { id: string; - createdAt: Date; - updatedAt: Date; } = await response.json(); - editorContext.setEditorStates((prev) => { - return { - ...prev, - currentWorkspace: { - id, - name, - address: getAPIUrl(`/workspace/${id}`).toString(), - createdAt, - updatedAt, - }, - }; - }); + const updated = await mutateCloudWorkspaces(); + const newWorkspace = updated?.find((ws) => ws.id === id); + setWorkspace(newWorkspace); } - function updateWorkspace(updatedWorkspace: RemoteWorkspace) { + async function updateWorkspace(workspaceId: string, name: string) { if (!editorContext) { throw new Error("Editor context is not available"); } - editorContext.setEditorStates((prev) => { - return { - ...prev, - currentWorkspace: updatedWorkspace, - }; + // Request to update the workspace + await fetchAPI(`/api/workspace/update`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + workspaceId, + }), }); + + await mutateCloudWorkspaces(); } - function selectWorkspace(workspaceId: string) { + function selectWorkspace(workspaceId: string | undefined) { if (!editorContext) { throw new Error("Editor context is not available"); } + if (!workspaceId) { + // Unselect workspace + setWorkspace(undefined); + return; + } + const selectedWorkspace = cloudWorkspaces?.find( (ws) => ws.id === workspaceId, ); @@ -105,12 +123,49 @@ export function useWorkspace() { throw new Error("Workspace not found"); } - editorContext.setEditorStates((prev) => { + setWorkspace(selectedWorkspace); + } + + async function deleteWorkspace(workspaceId: string) { + if (!editorContext) { + throw new Error("Editor context is not available"); + } + + const response = await fetchAPI(`/api/workspace/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ workspaceId }), + }); + + if (!response.ok) { + throw new Error("Failed to delete workspace"); + } + + setWorkspace(undefined); + mutateCloudWorkspaces(); + } + + async function refreshWorkspaceContent(platformApi: AbstractPlatformAPI) { + const projectUri = + editorContext?.persistSettings?.projectHomePath + + "/" + + editorContext?.editorStates.project; + const objects = await platformApi?.listPathContent(projectUri, { + include: "all", + isRecursive: true, + }); + + editorContext?.setEditorStates((prev) => { return { ...prev, - currentWorkspace: selectedWorkspace, + workspaceContent: objects, + explorerSelectedNodeRefs: [], }; }); + + console.log("Found project content:", objects); } return { @@ -119,5 +174,7 @@ export function useWorkspace() { createWorkspace, updateWorkspace, selectWorkspace, + deleteWorkspace, + refreshWorkspaceContent, }; } diff --git a/web/lib/platform-api/cloud/cloud-api.ts b/web/lib/platform-api/cloud/cloud-api.ts index 34bc620b..ea4032ed 100644 --- a/web/lib/platform-api/cloud/cloud-api.ts +++ b/web/lib/platform-api/cloud/cloud-api.ts @@ -1,3 +1,4 @@ +import { fetchAPI } from "@/lib/pulse-editor-website/backend"; import { FileSystemObject, ListPathOptions, @@ -5,10 +6,20 @@ import { ProjectInfo, RemoteWorkspace, } from "@/lib/types"; -import { AbstractPlatformAPI } from "../abstract-platform-api"; import toast from "react-hot-toast"; -import { fetchAPI } from "@/lib/pulse-editor-website/backend"; +import { AbstractPlatformAPI } from "../abstract-platform-api"; +/** + * Cloud Platform API + * + * This API will manage projects on the cloud server, + * so users can access their projects from any device. + * + * In case of need to access file system, + * Cloud API will interact with the remote workspace, + * but only limit to necessary file system operations. + * It will not save any project data on the remote workspace. + */ export class CloudAPI extends AbstractPlatformAPI { private workspace: RemoteWorkspace | undefined; @@ -64,8 +75,25 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - toast.error("Not implemented"); - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "list-path-content", + args: { uri, options }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to list path content"); + } + + const data = await response.json(); + return data; } async createProject(uri: string): Promise { @@ -113,8 +141,22 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - toast.error("Not implemented"); - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "create-folder", + args: { uri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to create folder"); + } } async createFile(uri: string): Promise { @@ -122,8 +164,22 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - toast.error("Not implemented"); - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "create-file", + args: { uri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to create file"); + } } async rename(oldUri: string, newUri: string): Promise { @@ -131,8 +187,22 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - toast.error("Not implemented"); - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "rename", + args: { oldUri, newUri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to rename file"); + } } async delete(uri: string): Promise { @@ -140,8 +210,22 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - toast.error("Not implemented"); - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "delete", + args: { uri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to delete file"); + } } // Reserved for cloud environment implementation @@ -150,21 +234,79 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "has-path", + args: { uri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to check if path exists"); + } + + const data = await response.json(); + return data === true; } + async readFile(uri: string): Promise { if (!this.workspace) { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "read-file", + args: { uri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to read file"); + } + + const data = await response.json(); + + return new File([data], uri); } + async writeFile(file: File, uri: string): Promise { if (!this.workspace) { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - throw new Error("Method not implemented."); + + const text = await file.text(); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "write-file", + args: { data: text, uri }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to write file"); + } + + return; } async copyFiles(from: string, to: string): Promise { @@ -172,7 +314,22 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "copy-files", + args: { from, to }, + }), + }); + + if (!response.ok) { + throw new Error("Failed to copy files"); + } } /* Persistent Settings */ @@ -186,6 +343,9 @@ export class CloudAPI extends AbstractPlatformAPI { } const settings: PersistentSettings = await response.json(); + + settings.projectHomePath = "/workspace"; + return settings; } @@ -213,7 +373,7 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - throw new Error("Method not implemented."); + return "/workspace"; } async createTerminal(): Promise { @@ -221,6 +381,23 @@ export class CloudAPI extends AbstractPlatformAPI { toast.error("No workspace selected"); throw new Error("No workspace selected"); } - throw new Error("Method not implemented."); + + const response = await fetchAPI(`/api/workspace/platform-api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + workspaceId: this.workspace.id, + operation: "create-terminal", + }), + }); + + if (!response.ok) { + throw new Error("Failed to create terminal"); + } + + const data = await response.text(); + return data; } } diff --git a/web/lib/platform-api/electron/electron-api.ts b/web/lib/platform-api/electron/electron-api.ts index 071bf6f2..55eb7d47 100644 --- a/web/lib/platform-api/electron/electron-api.ts +++ b/web/lib/platform-api/electron/electron-api.ts @@ -93,17 +93,17 @@ export class ElectronAPI extends AbstractPlatformAPI { async getPersistentSettings(): Promise { const persistentSettings: PersistentSettings = - await this.electronAPI?.loadSettings(); + await this.electronAPI?.getPersistentSettings(); return persistentSettings; } async setPersistentSettings(settings: PersistentSettings): Promise { - await this.electronAPI?.saveSettings(settings); + await this.electronAPI?.setPersistentSettings(settings); } async resetPersistentSettings(): Promise { - await this.electronAPI?.saveSettings({}); + await this.electronAPI?.setPersistentSettings({}); } async getInstallationPath(): Promise { diff --git a/web/lib/platform-api/remote/remote-api.ts b/web/lib/platform-api/remote/remote-api.ts new file mode 100644 index 00000000..5bb15cf6 --- /dev/null +++ b/web/lib/platform-api/remote/remote-api.ts @@ -0,0 +1,7 @@ +/* + Unlike cloud-api which handles project management on the cloud, + this will provide full desktop-like environment for users. + This means users will manage projects inside their remote workspace, + and nothing is saved on the cloud except for the workspace connection + info. +*/ \ No newline at end of file diff --git a/web/lib/pulse-editor-website/backend.ts b/web/lib/pulse-editor-website/backend.ts index 6739f966..8e8fb167 100644 --- a/web/lib/pulse-editor-website/backend.ts +++ b/web/lib/pulse-editor-website/backend.ts @@ -1,3 +1,8 @@ +import { CapacitorHttp } from "@capacitor/core"; +import { Preferences } from "@capacitor/preferences"; +import { PlatformEnum } from "../enums"; +import { getPlatform } from "../platform-api/platform-checker"; + export async function fetchAPI( relativeUrl: string | URL, options?: RequestInit, @@ -6,6 +11,59 @@ export async function fetchAPI( const url = typeof relativeUrl === "string" ? getAPIUrl(relativeUrl) : relativeUrl; + /* + Use Capacitor Http plugin for native http requests. + The native fetch will include cookies, + and when response is processed by client, + the cookies are set in the webview automatically. + */ + if (getPlatform() === PlatformEnum.Capacitor) { + // attach cookie manually + const tokenPref = await Preferences.get({ + key: "pulse-editor.session-token", + }); + const expPref = await Preferences.get({ + key: "pulse-editor.session-expiration", + }); + const token = tokenPref.value; + const exp = expPref.value; + + const headers = new Headers(options?.headers ?? {}); + if (token) { + headers.append( + "Cookie", + `pulse-editor.session-token=${token}; Path=/; Expires=${exp}; SameSite=None; Secure; ${process.env.NEXT_PUBLIC_BACKEND_URL ? "Domain=" + new URL(process.env.NEXT_PUBLIC_BACKEND_URL).hostname : ""}`, + ); + options = { + ...options, + headers, + }; + } + + const headerObj = Object.fromEntries(headers.entries()); + + const nativeResponse = await CapacitorHttp.request({ + url: url.toString(), + method: options?.method ?? "GET", + headers: headerObj, + data: options?.body, + }); + + console.log( + `${url}. \n\nRequest header: ${JSON.stringify(headerObj)} \n\nNative response: ${JSON.stringify(nativeResponse)} \n\nCookie: ${document.cookie}`, + ); + + const data = JSON.stringify(nativeResponse.data); + + // Convert CapacitorHttpResponse to Fetch Response + const fetchResponse = new Response(data, { + status: nativeResponse.status, + headers: nativeResponse.headers, + }); + + return fetchResponse; + } + return await fetch(url, { credentials: "include", ...options, diff --git a/web/lib/types.ts b/web/lib/types.ts index e7eea679..f10599f5 100644 --- a/web/lib/types.ts +++ b/web/lib/types.ts @@ -55,7 +55,6 @@ export type EditorStates = { isToolbarOpen: boolean; project?: string; - projectContent?: FileSystemObject[]; projectsInfo?: ProjectInfo[]; explorerSelectedNodeRefs: RefObject[]; @@ -69,8 +68,11 @@ export type EditorStates = { // The currently selected workspace currentWorkspace?: RemoteWorkspace; + workspaceContent?: FileSystemObject[]; + /* Auth */ isSigningIn?: boolean; + isRefreshSession?: boolean; /* Modals */ isAppInfoModalOpen?: boolean; @@ -164,11 +166,6 @@ export type FileSystemObject = { subDirItems?: FileSystemObject[]; }; -export type ProjectInfo = { - name: string; - ctime?: Date; -}; - export type TreeViewGroupRef = { startCreatingNewFolder: () => void; startCreatingNewFile: () => void; @@ -322,14 +319,6 @@ export type IMCContextType = { // #endregion // #region Pulse Editor Cloud -export type RemoteWorkspace = { - id: string; - name: string; - address: string; - createdAt?: Date; - updatedAt?: Date; -}; - export type Session = { user: { name: string; @@ -436,3 +425,30 @@ export type PlatformAssistantMessage = { }; // #endregion + +// #region Project + +export type ProjectInfo = { + name: string; + ctime?: Date; +}; + +export type ProjectAsset = { + name: string; + type: "file" | "workflow"; + // URI to download from remote or load from local + uri: string; +}; +// #endregion + +// #region Workspace +export type RemoteWorkspace = { + id: string; + name: string; + cpuLimit: string; + memoryLimit: string; + volumeSize: string; + createdAt?: Date; +}; + +// #endregion diff --git a/web/package.json b/web/package.json index 5bdc18f7..c96b75c8 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-editor/web", - "version": "0.1.1-alpha.13", + "version": "0.1.1-beta.0", "private": true, "type": "module", "scripts": { @@ -13,10 +13,13 @@ "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", + "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.4.3", "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", "@capacitor/keyboard": "^7.0.3", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.2", "@capacitor/status-bar": "^7.0.3", "@capawesome/capacitor-file-picker": "^7.2.0", @@ -26,7 +29,6 @@ "@langchain/community": "^0.3.49", "@langchain/core": "^0.3.66", "@langchain/openai": "^0.6.3", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin", "@pulse-editor/shared-utils": "^0.1.1-beta.55", "@ricky0123/vad-web": "^0.0.28", "@vercel/analytics": "^1.5.0",