mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-08-17 11:50:26 +03:00
feat(cli): add new acl subcommands (#8827)
* unify `CI` var handling, and lay foundation for `permission` subcommand * feat(cli/init&new): create `permissions` directory by default for plugins * generate permissions with consistent pathing on windows and unix * `pemrission create` initial implementation * add ls command * finalize `permission create` subcommand * `permission rm` subcommand * `permission add` subcommand * remove empty `permission copy` subcommand * clippy * `capability create` subcommand and move modules under `acl` directory * fix multiselect for `permission add` when capabilty doesn't have identifier * clippy * `create` -> `new` and change file * license headers * more license headers * clippy * Discard changes to examples/resources/src-tauri/.gitignore * fix build * cleanup --------- Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
parent
9be314f07a
commit
06d63d67a0
12
.changes/cli-acl-subcommands.md
Normal file
12
.changes/cli-acl-subcommands.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
'tauri-cli': 'patch:feat'
|
||||
'@tauri-apps/cli': 'patch:feat'
|
||||
---
|
||||
|
||||
Add new subcommands for managing permissions and cababilities:
|
||||
|
||||
- `tauri permission new`
|
||||
- `tauri permission add`
|
||||
- `tauri permission rm`
|
||||
- `tauri permission ls`
|
||||
- `tauri capability new`
|
@ -105,7 +105,7 @@ impl<'a> Builder<'a> {
|
||||
let _ = std::fs::remove_file(format!(
|
||||
"./permissions/{}/{}",
|
||||
acl::build::PERMISSION_SCHEMAS_FOLDER_NAME,
|
||||
acl::build::PERMISSION_SCHEMA_FILE_NAME
|
||||
acl::PERMISSION_SCHEMA_FILE_NAME
|
||||
));
|
||||
let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME));
|
||||
} else {
|
||||
|
@ -20,6 +20,7 @@ use schemars::{
|
||||
use super::{
|
||||
capability::{Capability, CapabilityFile},
|
||||
plugin::PermissionFile,
|
||||
PERMISSION_SCHEMA_FILE_NAME,
|
||||
};
|
||||
|
||||
/// Known name of the folder containing autogenerated permissions.
|
||||
@ -37,9 +38,6 @@ pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
|
||||
/// Known foldername of the permission schema files
|
||||
pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
|
||||
|
||||
/// Known filename of the permission schema JSON file
|
||||
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
|
||||
|
||||
/// Known filename of the permission documentation file
|
||||
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
|
||||
|
||||
|
@ -57,6 +57,7 @@ pub struct Capability {
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Configure remote URLs that can use the capability permissions.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub remote: Option<CapabilityRemote>,
|
||||
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
|
||||
#[serde(default = "default_capability_local")]
|
||||
@ -74,7 +75,7 @@ pub struct Capability {
|
||||
/// List of permissions attached to this capability. Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
|
||||
pub permissions: Vec<PermissionEntry>,
|
||||
/// Target platforms this capability applies. By default all platforms applies.
|
||||
#[serde(default = "default_platforms")]
|
||||
#[serde(default = "default_platforms", skip_serializing_if = "Vec::is_empty")]
|
||||
pub platforms: Vec<Target>,
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,9 @@ use thiserror::Error;
|
||||
|
||||
pub use self::{identifier::*, value::*};
|
||||
|
||||
/// Known filename of the permission schema JSON file
|
||||
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
|
||||
|
||||
#[cfg(feature = "build")]
|
||||
pub mod build;
|
||||
pub mod capability;
|
||||
@ -142,6 +145,12 @@ pub struct Scopes {
|
||||
pub deny: Option<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Scopes {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.allow.is_none() && self.deny.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Descriptions of explicit privileges of commands.
|
||||
///
|
||||
/// It can enable commands to be accessible in the frontend of the application.
|
||||
@ -151,12 +160,14 @@ pub struct Scopes {
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Permission {
|
||||
/// The version of the permission.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<NonZeroU64>,
|
||||
|
||||
/// A unique identifier for the permission.
|
||||
pub identifier: String,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Allowed or denied commands when using this permission.
|
||||
@ -164,7 +175,7 @@ pub struct Permission {
|
||||
pub commands: Commands,
|
||||
|
||||
/// Allowed or denied scoped when using this permission.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
|
||||
pub scope: Scopes,
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
|
||||
/// The default permission set of the plugin.
|
||||
///
|
||||
/// Works similarly to a permission with the "default" identifier.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct DefaultPermission {
|
||||
/// The version of the permission.
|
||||
@ -26,14 +26,14 @@ pub struct DefaultPermission {
|
||||
}
|
||||
|
||||
/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct PermissionFile {
|
||||
/// The default permission set for the plugin
|
||||
pub default: Option<DefaultPermission>,
|
||||
|
||||
/// A list of permissions sets defined
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub set: Vec<PermissionSet>,
|
||||
|
||||
/// A list of inlined permissions
|
||||
|
15
tooling/cli/Cargo.lock
generated
15
tooling/cli/Cargo.lock
generated
@ -4112,6 +4112,7 @@ version = "1.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
|
||||
dependencies = [
|
||||
"indexmap 2.2.3",
|
||||
"itoa 1.0.10",
|
||||
"ryu",
|
||||
"serde",
|
||||
@ -4697,6 +4698,7 @@ dependencies = [
|
||||
"ctrlc",
|
||||
"dialoguer",
|
||||
"duct",
|
||||
"dunce",
|
||||
"env_logger",
|
||||
"glob",
|
||||
"handlebars 5.1.0",
|
||||
@ -4737,7 +4739,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml 0.8.10",
|
||||
"toml_edit 0.21.1",
|
||||
"toml_edit 0.22.6",
|
||||
"unicode-width",
|
||||
"ureq",
|
||||
"url",
|
||||
@ -5103,17 +5105,6 @@ dependencies = [
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap 2.2.3",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.6"
|
||||
|
@ -52,12 +52,12 @@ anyhow = "1.0"
|
||||
tauri-bundler = { version = "2.0.1-beta.0", default-features = false, path = "../bundler" }
|
||||
colored = "2.0"
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
serde_json = "1.0"
|
||||
serde_json = { version = "1.0", features = [ "preserve_order" ] }
|
||||
notify = "6.1"
|
||||
notify-debouncer-mini = "0.4"
|
||||
shared_child = "1.0"
|
||||
duct = "0.13"
|
||||
toml_edit = "0.21"
|
||||
toml_edit = { version = "0.22", features = [ "serde" ] }
|
||||
json-patch = "1.2"
|
||||
tauri-utils = { version = "2.0.0-beta.4", path = "../../core/tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
|
||||
tauri-utils-v1 = { version = "1", package = "tauri-utils", features = [ "isolation", "schema", "config-json5", "config-toml" ] }
|
||||
@ -93,6 +93,7 @@ itertools = "0.11"
|
||||
local-ip-address = "0.5"
|
||||
css-color = "0.2"
|
||||
resvg = "0.36.0"
|
||||
dunce = "1"
|
||||
glob = "0.3"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
|
28
tooling/cli/src/acl/capability/mod.rs
Normal file
28
tooling/cli/src/acl/capability/mod.rs
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
mod new;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Manage or create capabilities for your app")]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
#[clap(alias = "create")]
|
||||
New(new::Options),
|
||||
}
|
||||
|
||||
pub fn command(cli: Cli) -> Result<()> {
|
||||
match cli.command {
|
||||
Commands::New(options) => new::command(options),
|
||||
}
|
||||
}
|
141
tooling/cli/src/acl/capability/new.rs
Normal file
141
tooling/cli/src/acl/capability/new.rs
Normal file
@ -0,0 +1,141 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use tauri_utils::acl::capability::{Capability, PermissionEntry};
|
||||
|
||||
use crate::{
|
||||
acl::FileFormat,
|
||||
helpers::{app_paths::tauri_dir, prompts},
|
||||
Result,
|
||||
};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Create a new permission file")]
|
||||
pub struct Options {
|
||||
/// Capability identifier.
|
||||
identifier: Option<String>,
|
||||
/// Capability description
|
||||
#[clap(long)]
|
||||
description: Option<String>,
|
||||
/// Capability windows
|
||||
#[clap(long)]
|
||||
windows: Option<Vec<String>>,
|
||||
/// Capability permissions
|
||||
#[clap(long)]
|
||||
permission: Option<Vec<String>>,
|
||||
/// Output file format.
|
||||
#[clap(long, default_value_t = FileFormat::Json)]
|
||||
format: FileFormat,
|
||||
/// The output file.
|
||||
#[clap(short, long)]
|
||||
out: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let identifier = match options.identifier {
|
||||
Some(i) => i,
|
||||
None => prompts::input("What's the capability identifier?", None, false, false)?.unwrap(),
|
||||
};
|
||||
|
||||
let description = match options.description {
|
||||
Some(d) => Some(d),
|
||||
None => prompts::input::<String>("What's the capability description?", None, false, true)?
|
||||
.and_then(|d| if d.is_empty() { None } else { Some(d) }),
|
||||
};
|
||||
|
||||
let windows = match options.windows.map(FromIterator::from_iter) {
|
||||
Some(w) => w,
|
||||
None => prompts::input::<String>(
|
||||
"Which windows should be affected by this? (comma separated)",
|
||||
Some("main".into()),
|
||||
false,
|
||||
false,
|
||||
)?
|
||||
.and_then(|d| {
|
||||
if d.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(d.split(',').map(ToString::to_string).collect())
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
let permissions: HashSet<String> = match options.permission.map(FromIterator::from_iter) {
|
||||
Some(p) => p,
|
||||
None => prompts::input::<String>(
|
||||
"What permissions to enable? (comma separated)",
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
)?
|
||||
.and_then(|p| {
|
||||
if p.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(p.split(',').map(ToString::to_string).collect())
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
let capability = Capability {
|
||||
identifier,
|
||||
description: description.unwrap_or_default(),
|
||||
remote: None,
|
||||
local: true,
|
||||
windows,
|
||||
webviews: Vec::new(),
|
||||
permissions: permissions
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
PermissionEntry::PermissionRef(
|
||||
p.clone()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| panic!("invalid permission {}", p)),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
platforms: Vec::new(),
|
||||
};
|
||||
|
||||
let path = match options.out {
|
||||
Some(o) => o.canonicalize()?,
|
||||
None => {
|
||||
let dir = tauri_dir();
|
||||
let capabilities_dir = dir.join("capabilities");
|
||||
capabilities_dir.join(format!(
|
||||
"{}.{}",
|
||||
capability.identifier,
|
||||
options.format.extension()
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if path.exists() {
|
||||
let msg = format!(
|
||||
"Capability already exists at {}",
|
||||
dunce::simplified(&path).display()
|
||||
);
|
||||
let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?;
|
||||
if overwrite {
|
||||
std::fs::remove_file(&path)?;
|
||||
} else {
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
std::fs::write(&path, options.format.serialize(&capability)?)?;
|
||||
|
||||
log::info!(action = "Created"; "capability at {}", dunce::simplified(&path).display());
|
||||
|
||||
Ok(())
|
||||
}
|
41
tooling/cli/src/acl/mod.rs
Normal file
41
tooling/cli/src/acl/mod.rs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub mod capability;
|
||||
pub mod permission;
|
||||
|
||||
#[derive(Debug, clap::ValueEnum, Clone)]
|
||||
enum FileFormat {
|
||||
Json,
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl Display for FileFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Json => write!(f, "json"),
|
||||
Self::Toml => write!(f, "toml"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileFormat {
|
||||
pub fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => "json",
|
||||
Self::Toml => "toml",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serialize>(&self, s: &S) -> crate::Result<String> {
|
||||
let contents = match self {
|
||||
Self::Json => serde_json::to_string_pretty(s)?,
|
||||
Self::Toml => toml_edit::ser::to_string_pretty(s)?,
|
||||
};
|
||||
Ok(contents)
|
||||
}
|
||||
}
|
147
tooling/cli/src/acl/permission/add.rs
Normal file
147
tooling/cli/src/acl/permission/add.rs
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::{
|
||||
helpers::{app_paths::tauri_dir_opt, prompts},
|
||||
Result,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum TomlOrJson {
|
||||
Toml(toml_edit::Document),
|
||||
Json(serde_json::Value),
|
||||
}
|
||||
|
||||
impl TomlOrJson {
|
||||
fn identifier(&self) -> &str {
|
||||
match self {
|
||||
TomlOrJson::Toml(t) => t
|
||||
.get("identifier")
|
||||
.and_then(|k| k.as_str())
|
||||
.unwrap_or_default(),
|
||||
TomlOrJson::Json(j) => j
|
||||
.get("identifier")
|
||||
.and_then(|k| k.as_str())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_permission(&mut self, idenitifer: String) {
|
||||
match self {
|
||||
TomlOrJson::Toml(t) => {
|
||||
let permissions = t.entry("permissions").or_insert_with(|| {
|
||||
toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::new()))
|
||||
});
|
||||
if let Some(permissions) = permissions.as_array_mut() {
|
||||
permissions.push(idenitifer)
|
||||
};
|
||||
}
|
||||
|
||||
TomlOrJson::Json(j) => {
|
||||
if let Some(o) = j.as_object_mut() {
|
||||
let permissions = o
|
||||
.entry("permissions")
|
||||
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
|
||||
if let Some(permissions) = permissions.as_array_mut() {
|
||||
permissions.push(serde_json::Value::String(idenitifer))
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn to_string(&self) -> Result<String> {
|
||||
Ok(match self {
|
||||
TomlOrJson::Toml(t) => t.to_string(),
|
||||
TomlOrJson::Json(j) => serde_json::to_string_pretty(&j)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn capability_from_path<P: AsRef<Path>>(path: P) -> Option<TomlOrJson> {
|
||||
match path.as_ref().extension().and_then(|o| o.to_str()) {
|
||||
Some("toml") => std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|c| c.parse::<toml_edit::Document>().ok())
|
||||
.map(TomlOrJson::Toml),
|
||||
Some("json") => std::fs::read(&path)
|
||||
.ok()
|
||||
.and_then(|c| serde_json::from_slice::<serde_json::Value>(&c).ok())
|
||||
.map(TomlOrJson::Json),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Add a permission to capabilities")]
|
||||
pub struct Options {
|
||||
/// Permission to remove.
|
||||
identifier: String,
|
||||
/// Capability to add the permission to.
|
||||
capability: Option<String>,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let dir = match tauri_dir_opt() {
|
||||
Some(t) => t,
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
|
||||
let capabilities_dir = dir.join("capabilities");
|
||||
if !capabilities_dir.exists() {
|
||||
anyhow::bail!(
|
||||
"Couldn't find capabilities directory at {}",
|
||||
dunce::simplified(&capabilities_dir).display()
|
||||
);
|
||||
}
|
||||
|
||||
let capabilities = std::fs::read_dir(&capabilities_dir)?
|
||||
.flatten()
|
||||
.filter(|e| e.file_type().map(|e| e.is_file()).unwrap_or_default())
|
||||
.filter_map(|e| {
|
||||
let path = e.path();
|
||||
capability_from_path(&path).and_then(|capability| match &options.capability {
|
||||
Some(c) => (c == capability.identifier()).then_some((capability, path)),
|
||||
None => Some((capability, path)),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut capabilities = if capabilities.len() > 1 {
|
||||
let selections = prompts::multiselect(
|
||||
"Choose which capabilities to add the permission to:",
|
||||
capabilities
|
||||
.iter()
|
||||
.map(|(c, p)| {
|
||||
let id = c.identifier();
|
||||
if id.is_empty() {
|
||||
dunce::simplified(p).to_str().unwrap_or_default()
|
||||
} else {
|
||||
id
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
None,
|
||||
)?;
|
||||
selections
|
||||
.into_iter()
|
||||
.map(|idx| capabilities[idx].clone())
|
||||
.collect()
|
||||
} else {
|
||||
capabilities
|
||||
};
|
||||
|
||||
for (capability, path) in &mut capabilities {
|
||||
capability.insert_permission(options.identifier.clone());
|
||||
std::fs::write(&path, capability.to_string()?)?;
|
||||
log::info!(action = "Added"; "permission `{}` to `{}` at {}", options.identifier, capability.identifier(), dunce::simplified(path).display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
148
tooling/cli/src/acl/permission/ls.rs
Normal file
148
tooling/cli/src/acl/permission/ls.rs
Normal file
@ -0,0 +1,148 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::{helpers::app_paths::tauri_dir, Result};
|
||||
use colored::Colorize;
|
||||
use tauri_utils::acl::plugin::Manifest;
|
||||
|
||||
use std::{collections::BTreeMap, fs::read_to_string};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "List permissions available to your application")]
|
||||
pub struct Options {
|
||||
/// Name of the plugin to list permissions.
|
||||
plugin: Option<String>,
|
||||
/// Permission identifier filter.
|
||||
#[clap(short, long)]
|
||||
filter: Option<String>,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let tauri_dir = tauri_dir();
|
||||
let plugin_manifests_path = tauri_dir
|
||||
.join("gen")
|
||||
.join("schemas")
|
||||
.join("plugin-manifests.json");
|
||||
|
||||
if plugin_manifests_path.exists() {
|
||||
let plugin_manifest_json = read_to_string(&plugin_manifests_path)?;
|
||||
let acl = serde_json::from_str::<BTreeMap<String, Manifest>>(&plugin_manifest_json)?;
|
||||
|
||||
for (plugin, manifest) in acl {
|
||||
if options
|
||||
.plugin
|
||||
.as_ref()
|
||||
.map(|p| p != &plugin)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
if let Some(default) = manifest.default_permission {
|
||||
if options
|
||||
.filter
|
||||
.as_ref()
|
||||
.map(|f| "default".contains(f))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
permissions.push(format!(
|
||||
"{}:{}\n{}\nPermissions: {}",
|
||||
plugin.magenta(),
|
||||
"default".cyan(),
|
||||
default.description,
|
||||
default
|
||||
.permissions
|
||||
.iter()
|
||||
.map(|c| c.cyan().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for set in manifest.permission_sets.values() {
|
||||
if options
|
||||
.filter
|
||||
.as_ref()
|
||||
.map(|f| set.identifier.contains(f))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
permissions.push(format!(
|
||||
"{}:{}\n{}\nPermissions: {}",
|
||||
plugin.magenta(),
|
||||
set.identifier.cyan(),
|
||||
set.description,
|
||||
set
|
||||
.permissions
|
||||
.iter()
|
||||
.map(|c| c.cyan().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for permission in manifest.permissions.into_values() {
|
||||
if options
|
||||
.filter
|
||||
.as_ref()
|
||||
.map(|f| permission.identifier.contains(f))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
permissions.push(format!(
|
||||
"{}:{}{}{}{}",
|
||||
plugin.magenta(),
|
||||
permission.identifier.cyan(),
|
||||
permission
|
||||
.description
|
||||
.map(|d| format!("\n{d}"))
|
||||
.unwrap_or_default(),
|
||||
if permission.commands.allow.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"\n{}: {}",
|
||||
"Allow commands".bold(),
|
||||
permission
|
||||
.commands
|
||||
.allow
|
||||
.iter()
|
||||
.map(|c| c.green().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
},
|
||||
if permission.commands.deny.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"\n{}: {}",
|
||||
"Deny commands".bold(),
|
||||
permission
|
||||
.commands
|
||||
.deny
|
||||
.iter()
|
||||
.map(|c| c.red().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.is_empty() {
|
||||
println!("{}\n", permissions.join("\n\n"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("permission file not found, please build your application once first")
|
||||
}
|
||||
}
|
39
tooling/cli/src/acl/permission/mod.rs
Normal file
39
tooling/cli/src/acl/permission/mod.rs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
mod add;
|
||||
mod ls;
|
||||
mod new;
|
||||
mod rm;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Manage or create permissions for your app or plugin")]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
#[clap(alias = "create")]
|
||||
New(new::Options),
|
||||
Add(add::Options),
|
||||
#[clap(alias = "remove")]
|
||||
Rm(rm::Options),
|
||||
#[clap(alias = "list")]
|
||||
Ls(ls::Options),
|
||||
}
|
||||
|
||||
pub fn command(cli: Cli) -> Result<()> {
|
||||
match cli.command {
|
||||
Commands::New(options) => new::command(options),
|
||||
Commands::Add(options) => add::command(options),
|
||||
Commands::Rm(options) => rm::command(options),
|
||||
Commands::Ls(options) => ls::command(options),
|
||||
}
|
||||
}
|
113
tooling/cli/src/acl/permission/new.rs
Normal file
113
tooling/cli/src/acl/permission/new.rs
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::{
|
||||
acl::FileFormat,
|
||||
helpers::{app_paths::tauri_dir_opt, prompts},
|
||||
Result,
|
||||
};
|
||||
|
||||
use tauri_utils::acl::{plugin::PermissionFile, Commands, Permission};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Create a new permission file")]
|
||||
pub struct Options {
|
||||
/// Permission identifier.
|
||||
identifier: Option<String>,
|
||||
/// Permission description
|
||||
#[clap(long)]
|
||||
description: Option<String>,
|
||||
/// List of commands to allow
|
||||
#[clap(short, long, use_value_delimiter = true)]
|
||||
allow: Option<Vec<String>>,
|
||||
/// List of commands to deny
|
||||
#[clap(short, long, use_value_delimiter = true)]
|
||||
deny: Option<Vec<String>>,
|
||||
/// Output file format.
|
||||
#[clap(long, default_value_t = FileFormat::Json)]
|
||||
format: FileFormat,
|
||||
/// The output file.
|
||||
#[clap(short, long)]
|
||||
out: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let identifier = match options.identifier {
|
||||
Some(i) => i,
|
||||
None => prompts::input("What's the permission identifier?", None, false, false)?.unwrap(),
|
||||
};
|
||||
|
||||
let description = match options.description {
|
||||
Some(d) => Some(d),
|
||||
None => prompts::input::<String>("What's the permission description?", None, false, true)?
|
||||
.and_then(|d| if d.is_empty() { None } else { Some(d) }),
|
||||
};
|
||||
|
||||
let allow: Vec<String> = options
|
||||
.allow
|
||||
.map(FromIterator::from_iter)
|
||||
.unwrap_or_default();
|
||||
let deny: Vec<String> = options
|
||||
.deny
|
||||
.map(FromIterator::from_iter)
|
||||
.unwrap_or_default();
|
||||
|
||||
let permission = Permission {
|
||||
version: None,
|
||||
identifier,
|
||||
description,
|
||||
commands: Commands { allow, deny },
|
||||
scope: Default::default(),
|
||||
};
|
||||
|
||||
let path = match options.out {
|
||||
Some(o) => o.canonicalize()?,
|
||||
None => {
|
||||
let dir = match tauri_dir_opt() {
|
||||
Some(t) => t,
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
let permissions_dir = dir.join("permissions");
|
||||
permissions_dir.join(format!(
|
||||
"{}.{}",
|
||||
permission.identifier,
|
||||
options.format.extension()
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if path.exists() {
|
||||
let msg = format!(
|
||||
"Permission already exists at {}",
|
||||
dunce::simplified(&path).display()
|
||||
);
|
||||
let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?;
|
||||
if overwrite {
|
||||
std::fs::remove_file(&path)?;
|
||||
} else {
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
std::fs::write(
|
||||
&path,
|
||||
options.format.serialize(&PermissionFile {
|
||||
default: None,
|
||||
set: Vec::new(),
|
||||
permission: vec![permission],
|
||||
})?,
|
||||
)?;
|
||||
|
||||
log::info!(action = "Created"; "permission at {}", dunce::simplified(&path).display());
|
||||
|
||||
Ok(())
|
||||
}
|
137
tooling/cli/src/acl/permission/rm.rs
Normal file
137
tooling/cli/src/acl/permission/rm.rs
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use clap::Parser;
|
||||
use tauri_utils::acl::{plugin::PermissionFile, PERMISSION_SCHEMA_FILE_NAME};
|
||||
|
||||
use crate::{acl::FileFormat, helpers::app_paths::tauri_dir_opt, Result};
|
||||
|
||||
fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> {
|
||||
for entry in std::fs::read_dir(dir)?.flatten() {
|
||||
let file_type = entry.file_type()?;
|
||||
let path = entry.path();
|
||||
if file_type.is_dir() {
|
||||
rm_permission_files(identifier, &path)?;
|
||||
} else {
|
||||
if path
|
||||
.file_name()
|
||||
.map(|name| name == PERMISSION_SCHEMA_FILE_NAME)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let (mut permission_file, format): (PermissionFile, FileFormat) =
|
||||
match path.extension().and_then(|o| o.to_str()) {
|
||||
Some("toml") => {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
(toml::from_str(&content)?, FileFormat::Toml)
|
||||
}
|
||||
Some("json") => {
|
||||
let content = std::fs::read(&path)?;
|
||||
(serde_json::from_slice(&content)?, FileFormat::Json)
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut updated;
|
||||
|
||||
if identifier == "default" {
|
||||
updated = permission_file.default.is_some();
|
||||
permission_file.default = None;
|
||||
} else {
|
||||
let set_len = permission_file.set.len();
|
||||
permission_file.set.retain(|s| s.identifier != identifier);
|
||||
updated = permission_file.set.len() != set_len;
|
||||
|
||||
let permission_len = permission_file.permission.len();
|
||||
permission_file
|
||||
.permission
|
||||
.retain(|s| s.identifier != identifier);
|
||||
updated = updated || permission_file.permission.len() != permission_len;
|
||||
}
|
||||
|
||||
// if the file is empty, let's remove it
|
||||
if permission_file.default.is_none()
|
||||
&& permission_file.set.is_empty()
|
||||
&& permission_file.permission.is_empty()
|
||||
{
|
||||
std::fs::remove_file(&path)?;
|
||||
log::info!(action = "Removed"; "file {}", dunce::simplified(&path).display());
|
||||
} else if updated {
|
||||
std::fs::write(&path, format.serialize(&permission_file)?)?;
|
||||
log::info!(action = "Removed"; "permission {identifier} from {}", dunce::simplified(&path).display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> {
|
||||
for entry in std::fs::read_dir(dir)?.flatten() {
|
||||
let file_type = entry.file_type()?;
|
||||
if file_type.is_file() {
|
||||
let path = entry.path();
|
||||
match path.extension().and_then(|o| o.to_str()) {
|
||||
Some("toml") => {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
if let Ok(mut value) = content.parse::<toml_edit::Document>() {
|
||||
if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) {
|
||||
let prev_len = permissions.len();
|
||||
permissions.retain(|p| p.as_str().map(|p| p != identifier).unwrap_or(false));
|
||||
if prev_len != permissions.len() {
|
||||
std::fs::write(&path, value.to_string())?;
|
||||
log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("json") => {
|
||||
let content = std::fs::read(&path)?;
|
||||
if let Ok(mut value) = serde_json::from_slice::<serde_json::Value>(&content) {
|
||||
if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) {
|
||||
let prev_len = permissions.len();
|
||||
permissions.retain(|p| p.as_str().map(|p| p != identifier).unwrap_or(false));
|
||||
if prev_len != permissions.len() {
|
||||
std::fs::write(&path, serde_json::to_vec_pretty(&value)?)?;
|
||||
log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Remove a permission file, and its reference from any capability")]
|
||||
pub struct Options {
|
||||
/// Permission to remove.
|
||||
identifier: String,
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let permissions_dir = std::env::current_dir()?.join("permissions");
|
||||
if permissions_dir.exists() {
|
||||
rm_permission_files(&options.identifier, &permissions_dir)?;
|
||||
}
|
||||
|
||||
if let Some(tauri_dir) = tauri_dir_opt() {
|
||||
let capabilities_dir = tauri_dir.join("capabilities");
|
||||
if capabilities_dir.exists() {
|
||||
rm_permission_from_capabilities(&options.identifier, &capabilities_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -60,12 +60,11 @@ pub struct Options {
|
||||
/// Command line arguments passed to the runner. Use `--` to explicitly mark the start of the arguments.
|
||||
pub args: Vec<String>,
|
||||
/// Skip prompting for values
|
||||
#[clap(long)]
|
||||
#[clap(long, env = "CI")]
|
||||
pub ci: bool,
|
||||
}
|
||||
|
||||
pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
|
||||
options.ci = options.ci || std::env::var("CI").is_ok();
|
||||
let ci = options.ci;
|
||||
|
||||
let target = options
|
||||
|
@ -66,14 +66,16 @@ fn lookup<F: Fn(&PathBuf) -> bool>(dir: &Path, checker: F) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_tauri_dir() -> PathBuf {
|
||||
let cwd = current_dir().expect("failed to read cwd");
|
||||
pub fn tauri_dir_opt() -> Option<PathBuf> {
|
||||
let Ok(cwd) = current_dir() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if cwd.join(ConfigFormat::Json.into_file_name()).exists()
|
||||
|| cwd.join(ConfigFormat::Json5.into_file_name()).exists()
|
||||
|| cwd.join(ConfigFormat::Toml.into_file_name()).exists()
|
||||
{
|
||||
return cwd;
|
||||
return Some(cwd);
|
||||
}
|
||||
|
||||
let src_tauri = cwd.join("src-tauri");
|
||||
@ -83,12 +85,23 @@ fn get_tauri_dir() -> PathBuf {
|
||||
.exists()
|
||||
|| src_tauri.join(ConfigFormat::Toml.into_file_name()).exists()
|
||||
{
|
||||
return src_tauri;
|
||||
return Some(src_tauri);
|
||||
}
|
||||
|
||||
lookup(&cwd, |path| folder_has_configuration_file(Target::Linux, path) || is_configuration_file(Target::Linux, path))
|
||||
.map(|p| if p.is_dir() { p } else { p.parent().unwrap().to_path_buf() })
|
||||
.unwrap_or_else(||
|
||||
lookup(&cwd, |path| {
|
||||
folder_has_configuration_file(Target::Linux, path) || is_configuration_file(Target::Linux, path)
|
||||
})
|
||||
.map(|p| {
|
||||
if p.is_dir() {
|
||||
p
|
||||
} else {
|
||||
p.parent().unwrap().to_path_buf()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tauri_dir() -> PathBuf {
|
||||
tauri_dir_opt().unwrap_or_else(||
|
||||
panic!("Couldn't recognize the current folder as a Tauri project. It must contain a `{}`, `{}` or `{}` file in any subfolder.",
|
||||
ConfigFormat::Json.into_file_name(),
|
||||
ConfigFormat::Json5.into_file_name(),
|
||||
@ -116,11 +129,6 @@ fn get_app_dir() -> Option<PathBuf> {
|
||||
|
||||
pub fn app_dir() -> &'static PathBuf {
|
||||
static APP_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
APP_DIR.get_or_init(|| {
|
||||
get_app_dir().unwrap_or_else(|| get_tauri_dir().parent().unwrap().to_path_buf())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tauri_dir() -> PathBuf {
|
||||
get_tauri_dir()
|
||||
APP_DIR
|
||||
.get_or_init(|| get_app_dir().unwrap_or_else(|| tauri_dir().parent().unwrap().to_path_buf()))
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ pub mod config;
|
||||
pub mod flock;
|
||||
pub mod framework;
|
||||
pub mod npm;
|
||||
pub mod prompts;
|
||||
pub mod template;
|
||||
pub mod updater_signature;
|
||||
pub mod web_dev_server;
|
||||
|
57
tooling/cli/src/helpers/prompts.rs
Normal file
57
tooling/cli/src/helpers/prompts.rs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
pub fn input<T>(
|
||||
prompt: &str,
|
||||
initial: Option<T>,
|
||||
skip: bool,
|
||||
allow_empty: bool,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
T: Clone + FromStr + Display + ToString,
|
||||
T::Err: Display + std::fmt::Debug,
|
||||
{
|
||||
if skip {
|
||||
Ok(initial)
|
||||
} else {
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let mut builder = dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(allow_empty);
|
||||
|
||||
if let Some(v) = initial {
|
||||
builder = builder.with_initial_text(v.to_string());
|
||||
}
|
||||
|
||||
builder.interact_text().map(Some).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirm(prompt: &str, default: Option<bool>) -> Result<bool> {
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let mut builder = dialoguer::Confirm::with_theme(&theme).with_prompt(prompt);
|
||||
if let Some(default) = default {
|
||||
builder = builder.default(default);
|
||||
}
|
||||
builder.interact().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn multiselect<T: ToString>(
|
||||
prompt: &str,
|
||||
items: &[T],
|
||||
defaults: Option<&[bool]>,
|
||||
) -> Result<Vec<usize>> {
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let mut builder = dialoguer::MultiSelect::with_theme(&theme)
|
||||
.with_prompt(prompt)
|
||||
.items(items);
|
||||
if let Some(defaults) = defaults {
|
||||
builder = builder.defaults(defaults);
|
||||
}
|
||||
builder.interact().map_err(Into::into)
|
||||
}
|
@ -5,23 +5,20 @@
|
||||
use crate::{
|
||||
helpers::{
|
||||
framework::{infer_from_package_json as infer_framework, Framework},
|
||||
resolve_tauri_path, template,
|
||||
prompts, resolve_tauri_path, template,
|
||||
},
|
||||
VersionMetadata,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env::current_dir,
|
||||
fmt::Display,
|
||||
fs::{read_to_string, remove_dir_all},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::Input;
|
||||
use handlebars::{to_json, Handlebars};
|
||||
use include_dir::{include_dir, Dir};
|
||||
use log::warn;
|
||||
@ -33,7 +30,7 @@ const TAURI_CONF_TEMPLATE: &str = include_str!("../templates/tauri.conf.json");
|
||||
#[clap(about = "Initialize a Tauri project in an existing directory")]
|
||||
pub struct Options {
|
||||
/// Skip prompting for values
|
||||
#[clap(long)]
|
||||
#[clap(long, env = "CI")]
|
||||
ci: bool,
|
||||
/// Force init to overwrite the src-tauri folder
|
||||
#[clap(short, long)]
|
||||
@ -76,7 +73,6 @@ struct InitDefaults {
|
||||
|
||||
impl Options {
|
||||
fn load(mut self) -> Result<Self> {
|
||||
self.ci = self.ci || std::env::var("CI").is_ok();
|
||||
let package_json_path = PathBuf::from(&self.directory).join("package.json");
|
||||
|
||||
let init_defaults = if package_json_path.exists() {
|
||||
@ -92,7 +88,7 @@ impl Options {
|
||||
};
|
||||
|
||||
self.app_name = self.app_name.map(|s| Ok(Some(s))).unwrap_or_else(|| {
|
||||
request_input(
|
||||
prompts::input(
|
||||
"What is your app name?",
|
||||
init_defaults.app_name.clone(),
|
||||
self.ci,
|
||||
@ -101,7 +97,7 @@ impl Options {
|
||||
})?;
|
||||
|
||||
self.window_title = self.window_title.map(|s| Ok(Some(s))).unwrap_or_else(|| {
|
||||
request_input(
|
||||
prompts::input(
|
||||
"What should the window title be?",
|
||||
init_defaults.app_name.clone(),
|
||||
self.ci,
|
||||
@ -109,7 +105,7 @@ impl Options {
|
||||
)
|
||||
})?;
|
||||
|
||||
self.frontend_dist = self.frontend_dist.map(|s| Ok(Some(s))).unwrap_or_else(|| request_input(
|
||||
self.frontend_dist = self.frontend_dist.map(|s| Ok(Some(s))).unwrap_or_else(|| prompts::input(
|
||||
r#"Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created?"#,
|
||||
init_defaults.framework.as_ref().map(|f| f.frontend_dist()),
|
||||
self.ci,
|
||||
@ -117,7 +113,7 @@ impl Options {
|
||||
))?;
|
||||
|
||||
self.dev_url = self.dev_url.map(|s| Ok(Some(s))).unwrap_or_else(|| {
|
||||
request_input(
|
||||
prompts::input(
|
||||
"What is the url of your dev server?",
|
||||
init_defaults.framework.map(|f| f.dev_url()),
|
||||
self.ci,
|
||||
@ -129,7 +125,7 @@ impl Options {
|
||||
.before_dev_command
|
||||
.map(|s| Ok(Some(s)))
|
||||
.unwrap_or_else(|| {
|
||||
request_input(
|
||||
prompts::input(
|
||||
"What is your frontend dev command?",
|
||||
Some("npm run dev".to_string()),
|
||||
self.ci,
|
||||
@ -140,7 +136,7 @@ impl Options {
|
||||
.before_build_command
|
||||
.map(|s| Ok(Some(s)))
|
||||
.unwrap_or_else(|| {
|
||||
request_input(
|
||||
prompts::input(
|
||||
"What is your frontend build command?",
|
||||
Some("npm run build".to_string()),
|
||||
self.ci,
|
||||
@ -283,29 +279,3 @@ pub fn command(mut options: Options) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn request_input<T>(
|
||||
prompt: &str,
|
||||
initial: Option<T>,
|
||||
skip: bool,
|
||||
allow_empty: bool,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
T: Clone + FromStr + Display + ToString,
|
||||
T::Err: Display + std::fmt::Debug,
|
||||
{
|
||||
if skip {
|
||||
Ok(initial)
|
||||
} else {
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let mut builder = Input::with_theme(&theme)
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(allow_empty);
|
||||
|
||||
if let Some(v) = initial {
|
||||
builder = builder.with_initial_text(v.to_string());
|
||||
}
|
||||
|
||||
builder.interact_text().map(Some).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
use anyhow::Context;
|
||||
pub use anyhow::Result;
|
||||
|
||||
mod acl;
|
||||
mod add;
|
||||
mod build;
|
||||
mod completions;
|
||||
@ -145,6 +146,8 @@ enum Commands {
|
||||
Icon(icon::Options),
|
||||
Signer(signer::Cli),
|
||||
Completions(completions::Options),
|
||||
Permission(acl::permission::Cli),
|
||||
Capability(acl::capability::Cli),
|
||||
}
|
||||
|
||||
fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
|
||||
@ -247,6 +250,8 @@ where
|
||||
Commands::Plugin(cli) => plugin::command(cli)?,
|
||||
Commands::Signer(cli) => signer::command(cli)?,
|
||||
Commands::Completions(options) => completions::command(options, cli_)?,
|
||||
Commands::Permission(options) => acl::permission::command(options)?,
|
||||
Commands::Capability(options) => acl::capability::command(options)?,
|
||||
Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
|
||||
#[cfg(target_os = "macos")]
|
||||
Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
|
||||
|
@ -64,6 +64,9 @@ pub struct Options {
|
||||
/// Open Android Studio
|
||||
#[clap(short, long)]
|
||||
pub open: bool,
|
||||
/// Skip prompting for values
|
||||
#[clap(long, env = "CI")]
|
||||
pub ci: bool,
|
||||
}
|
||||
|
||||
impl From<Options> for BuildOptions {
|
||||
@ -76,7 +79,7 @@ impl From<Options> for BuildOptions {
|
||||
bundles: None,
|
||||
config: options.config,
|
||||
args: Vec::new(),
|
||||
ci: false,
|
||||
ci: options.ci,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ pub struct Cli {
|
||||
#[clap(about = "Initialize Android target in the project")]
|
||||
pub struct InitOptions {
|
||||
/// Skip prompting for values
|
||||
#[clap(long)]
|
||||
#[clap(long, env = "CI")]
|
||||
ci: bool,
|
||||
/// Skips installing rust toolchains via rustup
|
||||
#[clap(long)]
|
||||
|
@ -38,14 +38,8 @@ pub fn command(
|
||||
) -> Result<()> {
|
||||
let wrapper = TextWrapper::default();
|
||||
|
||||
exec(
|
||||
target,
|
||||
&wrapper,
|
||||
ci || var_os("CI").is_some(),
|
||||
reinstall_deps,
|
||||
skip_targets_install,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("{:#}", e))?;
|
||||
exec(target, &wrapper, ci, reinstall_deps, skip_targets_install)
|
||||
.map_err(|e| anyhow::anyhow!("{:#}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,9 @@ pub struct Options {
|
||||
/// Open Xcode
|
||||
#[clap(short, long)]
|
||||
pub open: bool,
|
||||
/// Skip prompting for values
|
||||
#[clap(long, env = "CI")]
|
||||
pub ci: bool,
|
||||
}
|
||||
|
||||
impl From<Options> for BuildOptions {
|
||||
@ -72,7 +75,7 @@ impl From<Options> for BuildOptions {
|
||||
bundles: None,
|
||||
config: options.config,
|
||||
args: Vec::new(),
|
||||
ci: false,
|
||||
ci: options.ci,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ pub struct Cli {
|
||||
#[clap(about = "Initialize iOS target in the project")]
|
||||
pub struct InitOptions {
|
||||
/// Skip prompting for values
|
||||
#[clap(long)]
|
||||
#[clap(long, env = "CI")]
|
||||
ci: bool,
|
||||
/// Reinstall dependencies
|
||||
#[clap(short, long)]
|
||||
|
@ -2,7 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{helpers::template, Result};
|
||||
use crate::{
|
||||
helpers::{prompts::input, template},
|
||||
Result,
|
||||
};
|
||||
use clap::{Parser, Subcommand};
|
||||
use handlebars::Handlebars;
|
||||
|
||||
@ -56,7 +59,7 @@ pub fn command(cli: Cli) -> Result<()> {
|
||||
return Err(anyhow::anyhow!("android folder already exists"));
|
||||
}
|
||||
|
||||
let plugin_id = super::init::request_input(
|
||||
let plugin_id = input(
|
||||
"What should be the Android Package ID for your plugin?",
|
||||
Some(format!("com.plugin.{}", plugin_name)),
|
||||
false,
|
||||
|
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::helpers::prompts::input;
|
||||
use crate::Result;
|
||||
use crate::{
|
||||
helpers::{resolve_tauri_path, template},
|
||||
@ -9,7 +10,6 @@ use crate::{
|
||||
};
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::Input;
|
||||
use handlebars::{to_json, Handlebars};
|
||||
use heck::{ToKebabCase, ToPascalCase, ToSnakeCase};
|
||||
use include_dir::{include_dir, Dir};
|
||||
@ -18,10 +18,8 @@ use std::{
|
||||
collections::BTreeMap,
|
||||
env::current_dir,
|
||||
ffi::OsStr,
|
||||
fmt::Display,
|
||||
fs::{create_dir_all, remove_dir_all, File, OpenOptions},
|
||||
path::{Component, Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
pub const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/plugin");
|
||||
@ -145,7 +143,7 @@ pub fn command(mut options: Options) -> Result<()> {
|
||||
}
|
||||
|
||||
let plugin_id = if options.android || options.mobile {
|
||||
let plugin_id = request_input(
|
||||
let plugin_id = input(
|
||||
"What should be the Android Package ID for your plugin?",
|
||||
Some(format!("com.plugin.{}", plugin_name)),
|
||||
false,
|
||||
@ -218,6 +216,10 @@ pub fn command(mut options: Options) -> Result<()> {
|
||||
)
|
||||
.with_context(|| "failed to render plugin Android template")?;
|
||||
}
|
||||
|
||||
std::fs::create_dir(template_target_path.join("permissions"))
|
||||
.with_context(|| "failed to create `permissions` directory")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -278,29 +280,3 @@ pub fn generate_android_out_file(
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_input<T>(
|
||||
prompt: &str,
|
||||
initial: Option<T>,
|
||||
skip: bool,
|
||||
allow_empty: bool,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
T: Clone + FromStr + Display + ToString,
|
||||
T::Err: Display + std::fmt::Debug,
|
||||
{
|
||||
if skip {
|
||||
Ok(initial)
|
||||
} else {
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let mut builder = Input::with_theme(&theme)
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(allow_empty);
|
||||
|
||||
if let Some(v) = initial {
|
||||
builder = builder.with_initial_text(v.to_string());
|
||||
}
|
||||
|
||||
builder.interact_text().map(Some).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
@ -23,13 +23,11 @@ pub struct Options {
|
||||
#[clap(short, long)]
|
||||
force: bool,
|
||||
/// Skip prompting for values
|
||||
#[clap(long)]
|
||||
#[clap(long, env = "CI")]
|
||||
ci: bool,
|
||||
}
|
||||
|
||||
pub fn command(mut options: Options) -> Result<()> {
|
||||
options.ci = options.ci || std::env::var("CI").is_ok();
|
||||
|
||||
if options.ci && options.password.is_none() {
|
||||
log::warn!("Generating new private key without password. For security reasons, we recommend setting a password instead.");
|
||||
options.password.replace("".into());
|
||||
|
Loading…
Reference in New Issue
Block a user