@lamentis/naome 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/Cargo.lock +2 -2
  2. package/bin/naome-node.js +2 -1579
  3. package/bin/naome.js +19 -5
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +2 -1
  6. package/crates/naome-cli/src/main.rs +3 -0
  7. package/crates/naome-cli/src/quality_commands.rs +90 -2
  8. package/crates/naome-core/Cargo.toml +1 -1
  9. package/crates/naome-core/src/decision/checks.rs +64 -0
  10. package/crates/naome-core/src/decision/idle.rs +67 -0
  11. package/crates/naome-core/src/decision/json.rs +36 -0
  12. package/crates/naome-core/src/decision/states.rs +165 -0
  13. package/crates/naome-core/src/decision.rs +131 -353
  14. package/crates/naome-core/src/install_plan.rs +2 -0
  15. package/crates/naome-core/src/lib.rs +5 -3
  16. package/crates/naome-core/src/paths.rs +3 -1
  17. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  18. package/crates/naome-core/src/quality/adapters.rs +20 -67
  19. package/crates/naome-core/src/quality/cleanup.rs +13 -1
  20. package/crates/naome-core/src/quality/config.rs +8 -15
  21. package/crates/naome-core/src/quality/config_support.rs +24 -0
  22. package/crates/naome-core/src/quality/mod.rs +18 -0
  23. package/crates/naome-core/src/quality/scanner.rs +20 -8
  24. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  25. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  26. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  27. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  28. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  29. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  30. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  31. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  32. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  33. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  34. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  35. package/crates/naome-core/src/quality/types.rs +3 -0
  36. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  37. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  38. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  39. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  40. package/crates/naome-core/src/route/context.rs +180 -0
  41. package/crates/naome-core/src/route/execution.rs +96 -0
  42. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  43. package/crates/naome-core/src/route/execution_support.rs +57 -0
  44. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  45. package/crates/naome-core/src/route/git_ops.rs +72 -0
  46. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  47. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  48. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  49. package/crates/naome-core/src/route/worktree.rs +75 -0
  50. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  51. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  52. package/crates/naome-core/src/route.rs +44 -1217
  53. package/crates/naome-core/src/verification.rs +1 -0
  54. package/crates/naome-core/tests/decision.rs +24 -118
  55. package/crates/naome-core/tests/harness_health.rs +2 -0
  56. package/crates/naome-core/tests/quality.rs +12 -118
  57. package/crates/naome-core/tests/quality_structure.rs +116 -0
  58. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  59. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  60. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  61. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  62. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  63. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  64. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  65. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  66. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  67. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  68. package/crates/naome-core/tests/route.rs +1 -1376
  69. package/crates/naome-core/tests/route_baseline.rs +86 -0
  70. package/crates/naome-core/tests/route_completion.rs +141 -0
  71. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  72. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  73. package/crates/naome-core/tests/route_worktree.rs +54 -0
  74. package/crates/naome-core/tests/task_state.rs +60 -432
  75. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  76. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  77. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  78. package/crates/naome-core/tests/verification.rs +4 -45
  79. package/crates/naome-core/tests/verification_contract.rs +22 -78
  80. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  81. package/installer/agents.js +90 -0
  82. package/installer/context.js +67 -0
  83. package/installer/filesystem.js +166 -0
  84. package/installer/flows.js +84 -0
  85. package/installer/git-boundary.js +170 -0
  86. package/installer/git-hook-content.js +36 -0
  87. package/installer/git-hooks.js +134 -0
  88. package/installer/git-local.js +2 -0
  89. package/installer/git-shared.js +35 -0
  90. package/installer/harness-file-ops.js +140 -0
  91. package/installer/harness-files.js +56 -0
  92. package/installer/harness-verification.js +123 -0
  93. package/installer/install-plan.js +66 -0
  94. package/installer/main.js +25 -0
  95. package/installer/manifest-state.js +167 -0
  96. package/installer/native-build.js +24 -0
  97. package/installer/native-format.js +6 -0
  98. package/installer/native.js +162 -0
  99. package/installer/output.js +131 -0
  100. package/installer/version.js +32 -0
  101. package/native/darwin-arm64/naome +0 -0
  102. package/native/linux-x64/naome +0 -0
  103. package/package.json +2 -1
  104. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  105. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  106. package/templates/naome-root/.naome/bin/naome.js +25 -21
  107. package/templates/naome-root/.naome/manifest.json +4 -2
  108. package/templates/naome-root/.naome/repository-structure.json +90 -0
  109. package/templates/naome-root/.naome/verification.json +1 -0
  110. package/templates/naome-root/docs/naome/index.md +4 -3
  111. package/templates/naome-root/docs/naome/repository-quality.md +3 -0
  112. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  113. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -0,0 +1,73 @@
1
+ use std::path::Path;
2
+
3
+ use crate::git;
4
+ use crate::models::NaomeError;
5
+
6
+ use super::builtin_checks::run_quality_check;
7
+ pub(super) use super::quality_gate_config::QualityCheck;
8
+ use super::quality_gate_config::{user_diff_check_ids, verification_checks};
9
+ use super::quality_gate_snapshot::{
10
+ changed_path_snapshot, read_head_verification, sorted_path_set,
11
+ validate_changed_text_whitespace,
12
+ };
13
+
14
+ pub(super) fn run_user_diff_quality_gate(
15
+ root: &Path,
16
+ changed_paths: &[String],
17
+ ) -> Result<Vec<String>, NaomeError> {
18
+ if changed_paths.is_empty() {
19
+ return Err(NaomeError::new("No changed paths are available to commit."));
20
+ }
21
+ let verification = read_head_verification(root)?;
22
+ let checks = verification_checks(&verification);
23
+ let check_ids = user_diff_check_ids(&verification, changed_paths, &checks)?;
24
+
25
+ let initial_paths = sorted_path_set(changed_paths);
26
+ let mut previous_snapshot = changed_path_snapshot(root, changed_paths)?;
27
+ for _ in 0..3 {
28
+ validate_changed_text_whitespace(root, changed_paths)?;
29
+
30
+ for check_id in &check_ids {
31
+ let Some(check) = checks.get(check_id.as_str()) else {
32
+ return Err(NaomeError::new(format!(
33
+ "Quality check {check_id} is referenced but not defined."
34
+ )));
35
+ };
36
+ run_quality_check(root, check_id, check)?;
37
+ }
38
+
39
+ let current_paths = git::changed_paths(root)?;
40
+ let current_set = sorted_path_set(&current_paths);
41
+ if current_set != initial_paths {
42
+ return Err(NaomeError::new(format!(
43
+ "Quality checks changed the diff path set from [{}] to [{}].",
44
+ initial_paths.iter().cloned().collect::<Vec<_>>().join(", "),
45
+ current_set.iter().cloned().collect::<Vec<_>>().join(", ")
46
+ )));
47
+ }
48
+
49
+ validate_changed_text_whitespace(root, changed_paths)?;
50
+ if let Some(check) = checks.get("diff-check") {
51
+ run_quality_check(root, "diff-check", check)?;
52
+ }
53
+ let current_paths = git::changed_paths(root)?;
54
+ let current_set = sorted_path_set(&current_paths);
55
+ if current_set != initial_paths {
56
+ return Err(NaomeError::new(format!(
57
+ "Quality checks changed the diff path set from [{}] to [{}].",
58
+ initial_paths.iter().cloned().collect::<Vec<_>>().join(", "),
59
+ current_set.iter().cloned().collect::<Vec<_>>().join(", ")
60
+ )));
61
+ }
62
+
63
+ let next_snapshot = changed_path_snapshot(root, changed_paths)?;
64
+ if next_snapshot == previous_snapshot {
65
+ return Ok(check_ids);
66
+ }
67
+ previous_snapshot = next_snapshot;
68
+ }
69
+
70
+ Err(NaomeError::new(
71
+ "Quality checks did not stabilize the user-owned diff after three runs.",
72
+ ))
73
+ }
@@ -0,0 +1,126 @@
1
+ use std::collections::HashMap;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+ use crate::paths;
7
+
8
+ #[derive(Debug, Clone)]
9
+ pub(super) struct QualityCheck {
10
+ pub(super) command: String,
11
+ pub(super) cwd: String,
12
+ }
13
+
14
+ pub(super) fn verification_checks(verification: &Value) -> HashMap<&str, QualityCheck> {
15
+ let mut checks = HashMap::new();
16
+ let Some(items) = verification.get("checks").and_then(Value::as_array) else {
17
+ return checks;
18
+ };
19
+
20
+ for item in items {
21
+ let Some(id) = item.get("id").and_then(Value::as_str) else {
22
+ continue;
23
+ };
24
+ let Some(command) = item.get("command").and_then(Value::as_str) else {
25
+ continue;
26
+ };
27
+ let cwd = item
28
+ .get("cwd")
29
+ .and_then(Value::as_str)
30
+ .unwrap_or(".")
31
+ .to_string();
32
+ checks.insert(
33
+ id,
34
+ QualityCheck {
35
+ command: command.to_string(),
36
+ cwd,
37
+ },
38
+ );
39
+ }
40
+
41
+ checks
42
+ }
43
+
44
+ pub(super) fn user_diff_check_ids(
45
+ verification: &Value,
46
+ changed_paths: &[String],
47
+ checks: &HashMap<&str, QualityCheck>,
48
+ ) -> Result<Vec<String>, NaomeError> {
49
+ let mut ids = Vec::new();
50
+ if checks.contains_key("diff-check") {
51
+ push_unique_string(&mut ids, "diff-check");
52
+ }
53
+ if checks.contains_key("naome-harness-health") {
54
+ push_unique_string(&mut ids, "naome-harness-health");
55
+ }
56
+
57
+ let Some(change_types) = verification.get("changeTypes").and_then(Value::as_array) else {
58
+ return Err(no_quality_coverage());
59
+ };
60
+ if change_types.is_empty() {
61
+ return Err(no_quality_coverage());
62
+ }
63
+
64
+ let mut uncovered_paths = Vec::new();
65
+ for change_type in change_types {
66
+ let patterns = string_array(change_type.get("paths"));
67
+ if patterns.is_empty()
68
+ || !changed_paths
69
+ .iter()
70
+ .any(|path| paths::matches_any(path, &patterns))
71
+ {
72
+ continue;
73
+ }
74
+
75
+ for check_id in string_array(change_type.get("requiredChecks")) {
76
+ push_unique_string(&mut ids, &check_id);
77
+ }
78
+ }
79
+
80
+ for path in changed_paths {
81
+ let covered = change_types.iter().any(|change_type| {
82
+ let patterns = string_array(change_type.get("paths"));
83
+ !patterns.is_empty() && paths::matches_any(path, &patterns)
84
+ });
85
+ if !covered {
86
+ uncovered_paths.push(path.clone());
87
+ }
88
+ }
89
+
90
+ if !uncovered_paths.is_empty() {
91
+ return Err(NaomeError::new(format!(
92
+ "No quality coverage is configured for changed path(s): {}.",
93
+ uncovered_paths.join(", ")
94
+ )));
95
+ }
96
+ if ids.is_empty() {
97
+ return Err(NaomeError::new(
98
+ "No quality checks are configured for these changed paths.",
99
+ ));
100
+ }
101
+
102
+ Ok(ids)
103
+ }
104
+
105
+ fn no_quality_coverage() -> NaomeError {
106
+ NaomeError::new("No quality coverage is configured for user-owned changed paths.")
107
+ }
108
+
109
+ fn string_array(value: Option<&Value>) -> Vec<String> {
110
+ value
111
+ .and_then(Value::as_array)
112
+ .map(|items| {
113
+ items
114
+ .iter()
115
+ .filter_map(Value::as_str)
116
+ .map(ToString::to_string)
117
+ .collect()
118
+ })
119
+ .unwrap_or_default()
120
+ }
121
+
122
+ fn push_unique_string(values: &mut Vec<String>, value: &str) {
123
+ if !values.iter().any(|item| item == value) {
124
+ values.push(value.to_string());
125
+ }
126
+ }
@@ -0,0 +1,69 @@
1
+ use std::collections::BTreeSet;
2
+ use std::fs;
3
+ use std::path::Path;
4
+ use std::process::Command;
5
+
6
+ use serde_json::Value;
7
+
8
+ use crate::models::NaomeError;
9
+
10
+ pub(super) fn validate_changed_text_whitespace(
11
+ root: &Path,
12
+ changed_paths: &[String],
13
+ ) -> Result<(), NaomeError> {
14
+ for relative_path in changed_paths {
15
+ let path = root.join(relative_path);
16
+ if !path.is_file() {
17
+ continue;
18
+ }
19
+ let Ok(content) = fs::read_to_string(&path) else {
20
+ continue;
21
+ };
22
+ for (index, line) in content.lines().enumerate() {
23
+ if line.ends_with(' ') || line.ends_with('\t') {
24
+ return Err(NaomeError::new(format!(
25
+ "{relative_path}:{} has trailing whitespace.",
26
+ index + 1
27
+ )));
28
+ }
29
+ }
30
+ }
31
+ Ok(())
32
+ }
33
+
34
+ pub(super) fn read_head_verification(root: &Path) -> Result<Value, NaomeError> {
35
+ let output = Command::new("git")
36
+ .args(["show", "HEAD:.naome/verification.json"])
37
+ .current_dir(root)
38
+ .output()?;
39
+
40
+ if !output.status.success() {
41
+ return Err(NaomeError::new(
42
+ "No committed NAOME verification profile is available for user-diff quality gating.",
43
+ ));
44
+ }
45
+
46
+ Ok(serde_json::from_slice(&output.stdout)?)
47
+ }
48
+
49
+ pub(super) fn sorted_path_set(paths: &[String]) -> BTreeSet<String> {
50
+ paths.iter().cloned().collect()
51
+ }
52
+
53
+ pub(super) fn changed_path_snapshot(
54
+ root: &Path,
55
+ changed_paths: &[String],
56
+ ) -> Result<Vec<(String, Option<Vec<u8>>)>, NaomeError> {
57
+ let mut snapshot = Vec::new();
58
+ for relative_path in changed_paths {
59
+ let path = root.join(relative_path);
60
+ let content = if path.is_file() {
61
+ Some(fs::read(&path)?)
62
+ } else {
63
+ None
64
+ };
65
+ snapshot.push((relative_path.clone(), content));
66
+ }
67
+ snapshot.sort_by(|left, right| left.0.cmp(&right.0));
68
+ Ok(snapshot)
69
+ }
@@ -0,0 +1,75 @@
1
+ use std::path::Path;
2
+ use std::process::Command;
3
+
4
+ use crate::models::NaomeError;
5
+ use crate::route::{
6
+ git_ops::{command_output, git_head},
7
+ RouteWorktree,
8
+ };
9
+
10
+ use super::worktree_files::copy_local_harness_files;
11
+ use super::worktree_plan::{available_worktree_candidates, worktree_plan};
12
+
13
+ pub(super) fn create_isolated_task_worktree(
14
+ root: &Path,
15
+ prompt: &str,
16
+ ) -> Result<RouteWorktree, NaomeError> {
17
+ let name_head = task_worktree_name_head(root)?;
18
+ create_isolated_task_worktree_with_name_head(root, prompt, &name_head)
19
+ }
20
+
21
+ pub(super) fn create_isolated_task_worktree_with_name_head(
22
+ root: &Path,
23
+ prompt: &str,
24
+ name_head: &str,
25
+ ) -> Result<RouteWorktree, NaomeError> {
26
+ let base_head = git_head(root)?.ok_or_else(|| {
27
+ NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
28
+ })?;
29
+ let plan = worktree_plan(root, prompt, name_head)?;
30
+ for candidate in available_worktree_candidates(root, &plan)? {
31
+ let path_text = candidate.path.to_string_lossy().to_string();
32
+ let output = Command::new("git")
33
+ .args([
34
+ "worktree",
35
+ "add",
36
+ "-b",
37
+ &candidate.branch,
38
+ &path_text,
39
+ "HEAD",
40
+ ])
41
+ .current_dir(root)
42
+ .output()?;
43
+ if !output.status.success() {
44
+ return Err(NaomeError::new(command_output(&output)));
45
+ }
46
+
47
+ copy_local_harness_files(root, &candidate.path)?;
48
+
49
+ return Ok(RouteWorktree {
50
+ path: path_text,
51
+ branch: candidate.branch,
52
+ base_head,
53
+ source_root: root.to_string_lossy().to_string(),
54
+ });
55
+ }
56
+
57
+ Err(NaomeError::new(
58
+ "Cannot create a unique NAOME task worktree after 99 attempts.",
59
+ ))
60
+ }
61
+
62
+ pub(super) fn preflight_isolated_task_worktree(
63
+ root: &Path,
64
+ prompt: &str,
65
+ name_head: &str,
66
+ ) -> Result<(), NaomeError> {
67
+ let plan = worktree_plan(root, prompt, name_head)?;
68
+ available_worktree_candidates(root, &plan).map(|_| ())
69
+ }
70
+
71
+ pub(super) fn task_worktree_name_head(root: &Path) -> Result<String, NaomeError> {
72
+ git_head(root)?.ok_or_else(|| {
73
+ NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
74
+ })
75
+ }
@@ -0,0 +1,32 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::install_plan::{LOCAL_NATIVE_BINARY_PATHS, LOCAL_ONLY_MACHINE_OWNED_PATHS};
5
+ use crate::models::NaomeError;
6
+
7
+ pub(super) fn copy_local_harness_files(
8
+ source_root: &Path,
9
+ worktree_root: &Path,
10
+ ) -> Result<(), NaomeError> {
11
+ let mut paths = Vec::new();
12
+ paths.extend_from_slice(LOCAL_ONLY_MACHINE_OWNED_PATHS);
13
+ paths.extend_from_slice(LOCAL_NATIVE_BINARY_PATHS);
14
+
15
+ for relative_path in paths {
16
+ if relative_path == ".naome/archive" || relative_path == ".naome/task-journal.jsonl" {
17
+ continue;
18
+ }
19
+
20
+ let source = source_root.join(relative_path);
21
+ if !source.is_file() {
22
+ continue;
23
+ }
24
+ let destination = worktree_root.join(relative_path);
25
+ if let Some(parent) = destination.parent() {
26
+ fs::create_dir_all(parent)?;
27
+ }
28
+ fs::copy(&source, &destination)?;
29
+ }
30
+
31
+ Ok(())
32
+ }
@@ -0,0 +1,131 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use crate::models::NaomeError;
6
+ use crate::route::{
7
+ git_ops::{command_output, git_output},
8
+ MAX_NAOME_TASK_WORKTREES,
9
+ };
10
+
11
+ pub(super) struct WorktreePlan {
12
+ pub(super) base: PathBuf,
13
+ pub(super) slug: String,
14
+ pub(super) short_head: String,
15
+ }
16
+
17
+ pub(super) struct WorktreeCandidate {
18
+ pub(super) path: PathBuf,
19
+ pub(super) branch: String,
20
+ }
21
+
22
+ pub(super) fn worktree_plan(
23
+ root: &Path,
24
+ prompt: &str,
25
+ name_head: &str,
26
+ ) -> Result<WorktreePlan, NaomeError> {
27
+ let common_git_dir = git_common_dir(root)?;
28
+ let base = common_git_dir.join("naome").join("worktrees");
29
+ fs::create_dir_all(&base)?;
30
+ let worktree_count = existing_naome_task_worktree_count(&base)?;
31
+ if worktree_count >= MAX_NAOME_TASK_WORKTREES {
32
+ return Err(NaomeError::new(format!(
33
+ "Too many NAOME task worktrees are present ({worktree_count}). Finish or remove old task worktrees before creating another isolated task worktree."
34
+ )));
35
+ }
36
+
37
+ Ok(WorktreePlan {
38
+ base,
39
+ slug: prompt_slug(prompt),
40
+ short_head: name_head.chars().take(12).collect(),
41
+ })
42
+ }
43
+
44
+ pub(super) fn available_worktree_candidates(
45
+ root: &Path,
46
+ plan: &WorktreePlan,
47
+ ) -> Result<Vec<WorktreeCandidate>, NaomeError> {
48
+ let mut candidates = Vec::new();
49
+ for attempt in 1..100 {
50
+ let suffix = if attempt == 1 {
51
+ String::new()
52
+ } else {
53
+ format!("-{attempt}")
54
+ };
55
+ let name = format!("{}-{}{}", plan.slug, plan.short_head, suffix);
56
+ let branch = format!("naome/task/{name}");
57
+ let path = plan.base.join(&name);
58
+ if !path.exists() && !git_branch_exists(root, &branch)? {
59
+ candidates.push(WorktreeCandidate { path, branch });
60
+ return Ok(candidates);
61
+ }
62
+ }
63
+
64
+ Err(NaomeError::new(
65
+ "Cannot create a unique NAOME task worktree after 99 attempts.",
66
+ ))
67
+ }
68
+
69
+ fn existing_naome_task_worktree_count(worktree_base: &Path) -> Result<usize, NaomeError> {
70
+ let mut count = 0;
71
+ for entry in fs::read_dir(worktree_base)? {
72
+ let entry = entry?;
73
+ if entry.file_type()?.is_dir() {
74
+ count += 1;
75
+ }
76
+ }
77
+ Ok(count)
78
+ }
79
+
80
+ fn git_common_dir(root: &Path) -> Result<PathBuf, NaomeError> {
81
+ let output = git_output(root, &["rev-parse", "--git-common-dir"])?;
82
+ if !output.status.success() {
83
+ return Err(NaomeError::new(command_output(&output)));
84
+ }
85
+
86
+ let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
87
+ let path = PathBuf::from(value);
88
+ if path.is_absolute() {
89
+ Ok(path)
90
+ } else {
91
+ Ok(root.join(path))
92
+ }
93
+ }
94
+
95
+ fn git_branch_exists(root: &Path, branch: &str) -> Result<bool, NaomeError> {
96
+ let ref_name = format!("refs/heads/{branch}");
97
+ let output = Command::new("git")
98
+ .args(["show-ref", "--verify", "--quiet", &ref_name])
99
+ .current_dir(root)
100
+ .output()?;
101
+ match output.status.code() {
102
+ Some(0) => Ok(true),
103
+ Some(1) => Ok(false),
104
+ _ => Err(NaomeError::new(command_output(&output))),
105
+ }
106
+ }
107
+
108
+ fn prompt_slug(prompt: &str) -> String {
109
+ let mut slug = String::new();
110
+ let mut previous_dash = false;
111
+ for character in prompt.chars().flat_map(char::to_lowercase) {
112
+ if character.is_ascii_alphanumeric() {
113
+ slug.push(character);
114
+ previous_dash = false;
115
+ } else if !previous_dash && !slug.is_empty() {
116
+ slug.push('-');
117
+ previous_dash = true;
118
+ }
119
+
120
+ if slug.len() >= 40 {
121
+ break;
122
+ }
123
+ }
124
+
125
+ let slug = slug.trim_matches('-').to_string();
126
+ if slug.is_empty() {
127
+ "task".to_string()
128
+ } else {
129
+ slug
130
+ }
131
+ }