OmniStacksStaticModules 2.0.0
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
11#include <unistd.h>
12
13#include <cstdio>
14
15#include "json.hpp"
16
17import std;
18import InterfaceLog;
19import ProjectSync;
20import ReminderSync;
21import TaskManager;
22import WorkflowUI;
23
24namespace {
25
26namespace fs = std::filesystem;
27
28enum class ExitCode : int {
29 ok = 0,
30 usage = 1,
31 failed = 2,
32};
33
35struct Runtime {
36 fs::path root;
37 fs::path workflow_dir;
39 std::optional<fs::path> inbox_library;
40
42 [[nodiscard]] WorkflowUI::Context ui_context() const {
44 .workflow_dir = workflow_dir,
45 .databases = databases,
46 .inbox_library = inbox_library,
47 };
48 }
49};
50
55std::expected<std::monostate, interface_log::LogError> initialize_log(
56 const std::filesystem::path& path) {
57 std::fflush(stdout);
58 const int stdout_copy = ::dup(STDOUT_FILENO);
59 if (stdout_copy < 0) {
60 return interface_log::init(path);
61 }
62 if (::dup2(STDERR_FILENO, STDOUT_FILENO) < 0) {
63 ::close(stdout_copy);
64 return interface_log::init(path);
65 }
66 auto result = interface_log::init(path);
67 std::fflush(stdout);
68 ::dup2(stdout_copy, STDOUT_FILENO);
69 ::close(stdout_copy);
70 return result;
71}
72
74fs::path executable_directory(const char* argv0) {
75 std::error_code error;
76 const fs::path executable = fs::weakly_canonical(fs::absolute(argv0), error);
77 return error ? fs::absolute(argv0).parent_path() : executable.parent_path();
78}
79
81fs::path find_source_root(fs::path start) {
82 std::error_code error;
83 start = fs::absolute(start, error);
84 while (!start.empty() && start != start.root_path()) {
85 if (fs::exists(start / "CMakeLists.txt")) {
86 return start;
87 }
88 start = start.parent_path();
89 }
90 return fs::current_path();
91}
92
97bool environment_flag(std::string_view name, bool fallback) {
98 const std::string key(name);
99 const char* value = std::getenv(key.c_str());
100 if (value == nullptr) {
101 return fallback;
102 }
103 if (*value == '\0') {
104 return false;
105 }
106 std::string normalized(value);
107 std::ranges::transform(normalized, normalized.begin(), [](unsigned char character) {
108 return static_cast<char>(std::tolower(character));
109 });
110 return normalized != "0" && normalized != "false" && normalized != "off" &&
111 normalized != "unchecked";
112}
113
119std::optional<std::string> ensure_runtime_layout(const fs::path& root) {
120 std::error_code error;
121 fs::create_directories(root / "data", error);
122 if (error) {
123 return std::format("cannot create data directory: {}", error.message());
124 }
125 fs::create_directories(root / "log", error);
126 if (error) {
127 return std::format("cannot create log directory: {}", error.message());
128 }
129
130 const auto create_if_missing = [](const fs::path& path, std::string_view content) {
131 // 只创建缺失文件;已有空文件/用户数据保持原样。
132 if (fs::exists(path)) {
133 return true;
134 }
135 std::ofstream output(path, std::ios::binary | std::ios::trunc);
136 if (!output) {
137 return false;
138 }
139 output << content;
140 output.flush();
141 return static_cast<bool>(output);
142 };
143
144 if (!create_if_missing(root / "data" / "Project.json",
145 R"({"library_paths":[],"nodes":[],"updateTime":""})")) {
146 return "cannot create data/Project.json";
147 }
148 if (!create_if_missing(root / "data" / "Stack.json", R"({"nodes":[],"updateTime":""})")) {
149 return "cannot create data/Stack.json";
150 }
151 if (!create_if_missing(root / "data" / "Timers.json", R"({"timers":{}})")) {
152 return "cannot create data/Timers.json";
153 }
154 return std::nullopt;
155}
156
161Runtime discover_runtime(const char* argv0) {
162 const fs::path executable_dir = executable_directory(argv0);
163 const char* configured_value = std::getenv("STACKS_WORKFLOW_DIR");
164 const fs::path configured = configured_value != nullptr && *configured_value != '\0'
165 ? fs::path(configured_value)
166 : fs::path{};
167 fs::path root;
168 std::error_code path_error;
169 const fs::path canonical_configured = fs::weakly_canonical(configured, path_error);
170 // Alfred 总以二进制路径启动;源码 CLI 则允许从任意子目录执行。
171 if (fs::exists(executable_dir / "data" / "Project.json") ||
172 fs::exists(executable_dir / "info.plist") ||
173 (!canonical_configured.empty() && !path_error && canonical_configured == executable_dir)) {
174 root = executable_dir;
175 } else {
176 root = find_source_root(fs::current_path());
177 }
178
179 fs::path workflow_dir;
180 if (!configured.empty()) {
181 workflow_dir = configured;
182 } else if (fs::exists(root.parent_path() / "info.plist")) {
183 workflow_dir = root.parent_path();
184 } else {
185 workflow_dir = root;
186 }
187
188 const fs::path project_db = root / "data" / "Project.json";
189 const fs::path stack_db = root / "data" / "Stack.json";
190 const fs::path timer_db = root / "data" / "Timers.json";
191
192 std::optional<fs::path> inbox_library;
193 // library_paths 的第一个路径是产品约定,与 `s i` 默认工作区一致。
194 std::ifstream input(project_db);
195 try {
196 nlohmann::json project;
197 if (input >> project && project.contains("library_paths") &&
198 project["library_paths"].is_array() && !project["library_paths"].empty() &&
199 project["library_paths"][0].is_string()) {
200 inbox_library = project["library_paths"][0].get<std::string>();
201 }
202 } catch (const std::exception&) {
203 }
204
205 return Runtime{
206 .root = root,
207 .workflow_dir = workflow_dir,
208 .databases = {project_db, stack_db, timer_db},
209 .inbox_library = inbox_library,
210 };
211}
212
215 std::println("Stacks_Tools 0.2");
216 std::println(" Stacks_Tools sync <library_path>");
217 std::println(
218 " Stacks_Tools add <project_or_stack_id> \"title|description\" [--start d] [--due d] "
219 "[--priority p]");
220 std::println(" Stacks_Tools inbox \"title|description\" [--start d] [--due d] [--priority p]");
221 std::println(" Stacks_Tools boards [query]");
222 std::println(" Stacks_Tools tasks [--board <id>] [--query <text>] [--todo-only]");
223 std::println(" Stacks_Tools week [anchor_date]");
224 std::println(" Stacks_Tools get <task_id>");
225 std::println(
226 " Stacks_Tools edit <task_id> [--title t] [--description d] [--start d] [--due d] "
227 "[--priority p]");
228 std::println(" Stacks_Tools due <task_id> <date>");
229 std::println(" Stacks_Tools start <task_id> <date>");
230 std::println(" Stacks_Tools schedule <task_id> <start_date> <due_date>");
231 std::println(" Stacks_Tools done|reopen|archive|delete <task_id>");
232 std::println(" Stacks_Tools timer-start|timer-stop <task_id>");
233 std::println(" Stacks_Tools timers");
234 std::println(" Stacks_Tools sync-reminder <task_id> reminders|omnifocus|both [--dry-run]");
235 std::println(" Stacks_Tools sync-week reminders|omnifocus|both [--dry-run]");
236 std::println(" Stacks_Tools reminder-script reminders|omnifocus");
237 std::println(" Stacks_Tools ui <query>");
238 std::println(" Stacks_Tools action <action_token>");
239}
240
243 std::println("{}\t{}\t{}\t{} / {}\t{}", task.id, task.done ? "done" : "todo",
244 task.due_date.empty() ? "-" : task.due_date, task.project_title, task.board_title,
245 task.title);
246}
247
249int run_sync(const Runtime& runtime, const fs::path& path) {
250 const auto result =
252 if (!result) {
253 std::println(std::cerr, "sync failed: {}", ProjectSync::sync_error_message(result.error()));
254 return static_cast<int>(ExitCode::failed);
255 }
256 std::println("synced: projects={} stacks={} failed_projects={}", result->project_count,
257 result->stack_count, result->failed_projects.size());
258 return static_cast<int>(ExitCode::ok);
259}
260
269int run_add(const Runtime& runtime, const std::string& target, const std::string& payload,
270 const std::vector<std::string>& values) {
271 // CLI 仍兼容 `title|description`,额外日期/优先级从后续 --option 读取。
272 const auto separator = payload.find('|');
273 const std::string title = separator == std::string::npos ? payload : payload.substr(0, separator);
274 const std::string description =
275 separator == std::string::npos ? std::string{} : payload.substr(separator + 1);
276 std::optional<std::string> start;
277 std::optional<std::string> due;
278 std::string priority;
279 // 这里只做 token 提取;未知/非法日期和优先级由 TaskManager 统一分类。
280 for (std::size_t index = 0; index + 1 < values.size(); ++index) {
281 if (values[index] == "--start") {
282 start = values[index + 1];
283 } else if (values[index] == "--due") {
284 due = values[index + 1];
285 } else if (values[index] == "--priority") {
286 priority = values[index + 1];
287 }
288 }
289 const auto result =
292 TaskManager::TaskDescription(description),
293 std::move(start), std::move(due), priority},
294 {runtime.databases, runtime.inbox_library});
295 if (!result) {
296 // 失败文本进入 stderr,脚本层据此切换 Basso 音效;stdout 不输出半结果。
297 std::println(std::cerr, "add failed: {}", TaskManager::task_error_message(result.error()));
298 return static_cast<int>(ExitCode::failed);
299 }
300 std::println("created: id={} kind={} file={}", result->task_id.value, result->target_kind,
301 result->task_file.string());
302 return static_cast<int>(ExitCode::ok);
303}
304
312int run_inbox(const Runtime& runtime, const std::string& payload,
313 const std::vector<std::string>& values) {
314 // Inbox CLI 与 add 共享相同字段/选项语法,但目标固定为 inbox。
315 const auto separator = payload.find('|');
316 const std::string title = separator == std::string::npos ? payload : payload.substr(0, separator);
317 const std::string description =
318 separator == std::string::npos ? std::string{} : payload.substr(separator + 1);
319 std::optional<std::string> start;
320 std::optional<std::string> due;
321 std::string priority;
322 for (std::size_t index = 0; index + 1 < values.size(); ++index) {
323 if (values[index] == "--start") {
324 start = values[index + 1];
325 } else if (values[index] == "--due") {
326 due = values[index + 1];
327 } else if (values[index] == "--priority") {
328 priority = values[index + 1];
329 }
330 }
331 const auto result = TaskManager::create_inbox_task(
333 TaskManager::TaskDescription(description), std::move(start),
334 std::move(due), priority},
335 {runtime.databases, runtime.inbox_library});
336 if (!result) {
337 std::println(std::cerr, "inbox failed: {}", TaskManager::task_error_message(result.error()));
338 return static_cast<int>(ExitCode::failed);
339 }
340 std::println("created: id={} file={}", result->task_id.value, result->task_file.string());
341 return static_cast<int>(ExitCode::ok);
342}
343
345std::vector<std::string> arguments(int argc, char* argv[], int start) {
346 std::vector<std::string> result;
347 for (int index = start; index < argc; ++index) {
348 result.emplace_back(argv[index]);
349 }
350 return result;
351}
352
354std::optional<std::string> option_value(const std::vector<std::string>& values,
355 std::string_view option) {
356 for (std::size_t index = 0; index + 1 < values.size(); ++index) {
357 if (values[index] == option) {
358 return values[index + 1];
359 }
360 }
361 return std::nullopt;
362}
363
365bool has_option(const std::vector<std::string>& values, std::string_view option) {
366 return std::ranges::find(values, option) != values.end();
367}
368
375int run_boards(const Runtime& runtime, std::string query) {
376 const auto result = TaskManager::list_boards(runtime.databases);
377 if (!result) {
378 std::println(std::cerr, "boards failed: {}", TaskManager::task_error_message(result.error()));
379 return static_cast<int>(ExitCode::failed);
380 }
381 for (const auto& board : *result) {
382 const std::string text = std::format("{} {} {}", board.id, board.project_title, board.title);
383 if (query.empty() || text.find(query) != std::string::npos) {
384 std::println("{}\t{}\t{} / {}", board.id, board.kind, board.project_title, board.title);
385 }
386 }
387 return static_cast<int>(ExitCode::ok);
388}
389
396int run_tasks(const Runtime& runtime, const std::vector<std::string>& values) {
398 if (const auto board = option_value(values, "--board")) {
399 options.target = TaskManager::TargetId(*board);
400 }
401 if (const auto query = option_value(values, "--query")) {
402 options.query = *query;
403 }
404 options.include_done = !has_option(values, "--todo-only");
405 const auto result = TaskManager::list_tasks(runtime.databases, options);
406 if (!result) {
407 std::println(std::cerr, "tasks failed: {}", TaskManager::task_error_message(result.error()));
408 return static_cast<int>(ExitCode::failed);
409 }
410 for (const auto& task : *result) {
411 print_task(task);
412 }
413 return static_cast<int>(ExitCode::ok);
414}
415
422int run_week(const Runtime& runtime, std::optional<std::string> anchor) {
423 const auto result = TaskManager::list_week_due(runtime.databases, std::move(anchor));
424 if (!result) {
425 std::println(std::cerr, "week failed: {}", TaskManager::task_error_message(result.error()));
426 return static_cast<int>(ExitCode::failed);
427 }
428 for (const auto& task : *result) {
429 print_task(task);
430 }
431 return static_cast<int>(ExitCode::ok);
432}
433
440int run_get(const Runtime& runtime, const std::string& id) {
441 const auto result = TaskManager::get_task(TaskManager::TaskId(id), runtime.databases);
442 if (!result) {
443 std::println(std::cerr, "get failed: {}", TaskManager::task_error_message(result.error()));
444 return static_cast<int>(ExitCode::failed);
445 }
446 std::println("id: {}", result->id);
447 std::println("title: {}", result->title);
448 std::println("description: {}", result->description);
449 std::println("project: {} ({})", result->project_title, result->project_id);
450 std::println("board: {} ({})", result->board_title, result->board_id);
451 std::println("startdate: {}", result->start_date);
452 std::println("duedate: {}", result->due_date);
453 std::println("priority: {}", result->priority.empty() ? "-" : result->priority);
454 std::println("done: {} progress: {} completed: {}", result->done, result->progress,
455 result->completed);
456 std::println("archived: {}", result->archived);
457 std::println("file: {}", result->file.string());
458 return static_cast<int>(ExitCode::ok);
459}
460
468int run_edit(const Runtime& runtime, const std::string& id,
469 const std::vector<std::string>& values) {
471 patch.title = option_value(values, "--title");
472 patch.description = option_value(values, "--description");
473 patch.start_date = option_value(values, "--start");
474 patch.due_date = option_value(values, "--due");
475 patch.priority = option_value(values, "--priority");
476 patch.clear_start_date = has_option(values, "--clear-start");
477 patch.clear_due_date = has_option(values, "--clear-due");
478 patch.clear_priority = has_option(values, "--clear-priority");
479 const auto result = TaskManager::update_task(TaskManager::TaskId(id), patch, runtime.databases);
480 if (!result) {
481 std::println(std::cerr, "edit failed: {}", TaskManager::task_error_message(result.error()));
482 return static_cast<int>(ExitCode::failed);
483 }
484 print_task(*result);
485 return static_cast<int>(ExitCode::ok);
486}
487
492template <typename Result>
493int print_task_result(const std::string& command, const Result& result) {
494 if (!result) {
495 std::println(std::cerr, "{} failed: {}", command,
496 TaskManager::task_error_message(result.error()));
497 return static_cast<int>(ExitCode::failed);
498 }
499 if constexpr (std::same_as<Result,
500 std::expected<TaskManager::TaskRecord, TaskManager::TaskError>>) {
501 print_task(*result);
502 } else if constexpr (std::same_as<Result, std::expected<TaskManager::TimerRecord,
504 std::println("timer started: task={} title={} at={}", result->task_id, result->title,
505 result->started_at);
506 } else if constexpr (std::same_as<Result, std::expected<TaskManager::StopTimerResult,
508 std::println("timer stopped: duration={} log_id={} file={}", result->duration_seconds,
509 result->timelog_id, result->task_file.string());
510 } else if constexpr (std::same_as<Result, std::expected<TaskManager::DeleteResult,
512 std::println("task moved to trash: {}", result->trash_file.string());
513 }
514 return static_cast<int>(ExitCode::ok);
515}
516
518ReminderSync::Provider parse_provider_or_default(const std::string& value, bool* valid) {
519 *valid = true;
520 if (value == "reminders") {
522 }
523 if (value == "omnifocus") {
525 }
526 if (value == "both") {
528 }
529 *valid = false;
531}
532
541int run_sync_reminder(const Runtime& runtime, const std::string& id,
542 const std::string& provider_text, bool dry_run) {
543 bool valid = false;
544 const ReminderSync::Provider provider = parse_provider_or_default(provider_text, &valid);
545 if (!valid) {
546 std::println(std::cerr, "provider must be reminders, omnifocus or both");
547 return static_cast<int>(ExitCode::usage);
548 }
549 const auto task = TaskManager::get_task(TaskManager::TaskId(id), runtime.databases);
550 if (!task) {
551 std::println(std::cerr, "sync failed: {}", TaskManager::task_error_message(task.error()));
552 return static_cast<int>(ExitCode::failed);
553 }
555 options.dry_run = dry_run;
556 const auto result =
558 .title = task->title,
559 .description = task->description,
560 .due_date = task->due_date,
561 .done = task->done},
562 provider, options);
563 if (!result) {
564 std::println(std::cerr, "sync failed: {}", ReminderSync::sync_error_message(result.error()));
565 return static_cast<int>(ExitCode::failed);
566 }
567 std::println("synced: reminders={} omnifocus={} marker={}", result->reminders_ok,
568 result->omnifocus_ok, result->marker);
569 return static_cast<int>(ExitCode::ok);
570}
571
579int run_sync_week(const Runtime& runtime, const std::string& provider_text, bool dry_run) {
580 bool valid = false;
581 const ReminderSync::Provider provider = parse_provider_or_default(provider_text, &valid);
582 if (!valid) {
583 std::println(std::cerr, "provider must be reminders, omnifocus or both");
584 return static_cast<int>(ExitCode::usage);
585 }
586 const auto tasks = TaskManager::list_week_due(runtime.databases);
587 if (!tasks) {
588 std::println(std::cerr, "sync-week failed: {}", TaskManager::task_error_message(tasks.error()));
589 return static_cast<int>(ExitCode::failed);
590 }
591 std::size_t succeeded = 0;
592 for (const auto& task : *tasks) {
594 options.dry_run = dry_run;
595 const auto result =
597 .title = task.title,
598 .description = task.description,
599 .due_date = task.due_date,
600 .done = task.done},
601 provider, options);
602 if (!result) {
603 std::println(std::cerr, "sync-week failed at {}: {}", task.id,
604 ReminderSync::sync_error_message(result.error()));
605 return static_cast<int>(ExitCode::failed);
606 }
607 ++succeeded;
608 }
609 std::println("sync-week ok: tasks={} dry_run={}", succeeded, dry_run);
610 return static_cast<int>(ExitCode::ok);
611}
612
619int run_ui(const Runtime& runtime, const std::string& query) {
620 std::cout << WorkflowUI::filter(runtime.ui_context(), query);
621 return static_cast<int>(ExitCode::ok);
622}
623
630int run_action(const Runtime& runtime, const std::string& token) {
631 const auto result = WorkflowUI::action(runtime.ui_context(), token);
632 if (!result) {
633 std::println("{}", result.error());
634 return static_cast<int>(ExitCode::failed);
635 }
636 std::println("{}", *result);
637 return static_cast<int>(ExitCode::ok);
638}
639
640} // namespace
641
642int main(int argc, char* argv[]) {
643 try {
644 // 阶段 1:无参数只打印帮助,不触碰运行时数据。
645 if (argc < 2) {
646 print_usage();
647 return static_cast<int>(ExitCode::usage);
648 }
649
650 // 阶段 2:发现根目录并确保 data/log/三个 JSON 存在。
651 Runtime runtime = discover_runtime(argv[0]);
652 if (const auto layout_error = ensure_runtime_layout(runtime.root); layout_error) {
653 std::println(std::cerr, "startup failed: {}", *layout_error);
654 return static_cast<int>(ExitCode::failed);
655 }
656
657 // 阶段 3:按 Alfred 配置启用日志;日志失败只警告,不影响业务命令。
658 const bool logging_enabled = environment_flag("STACKS_LOGGING", true);
659 interface_log::set_enabled(logging_enabled);
660 if (logging_enabled) {
661 const auto log_init = initialize_log(runtime.root / "log" / "interface.log");
662 if (!log_init) {
663 std::println(std::cerr, "warning: {}; in-memory log remains active",
664 interface_log::log_error_message(log_init.error()));
665 } else {
666 const std::size_t removed =
667 interface_log::cleanup_old_logs(runtime.root / "log" / "interface.log", 3);
668 const std::vector<std::string> invocation(argv, argv + argc);
670 interface_log::ApiName("main::invoke"),
671 interface_log::LogText(std::format(
672 "argc={} argv={}", argc,
673 invocation | std::views::join_with(' ') | std::ranges::to<std::string>())),
674 interface_log::LogText(std::format("runtime={} log_cleanup_removed={} developer={}",
675 runtime.root.string(), removed,
678 }
679 }
680
681 const std::string command = argv[1];
682
683 // 阶段 4:Alfred UI/action 是主要入口,保持最先匹配。
684 if (command == "ui") {
685 return run_ui(runtime, argc >= 3 ? argv[2] : "");
686 }
687 if (command == "action") {
688 if (argc < 3) {
689 std::println(std::cerr, "action requires an action token");
690 return static_cast<int>(ExitCode::usage);
691 }
692 return run_action(runtime, argv[2]);
693 }
694 if (command == "sync") {
695 if (argc < 3) {
696 std::println(std::cerr, "sync requires <library_path>");
697 return static_cast<int>(ExitCode::usage);
698 }
699 return run_sync(runtime, argv[2]);
700 }
701 if (command == "add") {
702 if (argc < 4) {
703 std::println(std::cerr, "add requires <target_id> and \"title|description\"");
704 return static_cast<int>(ExitCode::usage);
705 }
706 return run_add(runtime, argv[2], argv[3], arguments(argc, argv, 4));
707 }
708 if (command == "inbox") {
709 if (argc < 3) {
710 std::println(std::cerr, "inbox requires \"title|description\"");
711 return static_cast<int>(ExitCode::usage);
712 }
713 return run_inbox(runtime, argv[2], arguments(argc, argv, 3));
714 }
715 if (command == "boards") {
716 return run_boards(runtime, argc >= 3 ? argv[2] : "");
717 }
718 if (command == "tasks") {
719 return run_tasks(runtime, arguments(argc, argv, 2));
720 }
721 if (command == "week") {
722 return run_week(runtime, argc >= 3 ? std::optional<std::string>(argv[2]) : std::nullopt);
723 }
724 if (command == "get") {
725 if (argc < 3) {
726 std::println(std::cerr, "get requires <task_id>");
727 return static_cast<int>(ExitCode::usage);
728 }
729 return run_get(runtime, argv[2]);
730 }
731 if (command == "edit") {
732 if (argc < 3) {
733 std::println(std::cerr, "edit requires <task_id>");
734 return static_cast<int>(ExitCode::usage);
735 }
736 return run_edit(runtime, argv[2], arguments(argc, argv, 3));
737 }
738 if (command == "due" || command == "start" || command == "schedule") {
739 if (argc < 4) {
740 std::println(std::cerr, "{} requires task id and date argument(s)", command);
741 return static_cast<int>(ExitCode::usage);
742 }
743 if (command == "due") {
745 argv[3], runtime.databases));
746 }
747 if (command == "start") {
749 argv[3], runtime.databases));
750 }
751 if (argc < 5) {
752 std::println(std::cerr, "schedule requires start date and due date");
753 return static_cast<int>(ExitCode::usage);
754 }
755 return print_task_result(
756 command, TaskManager::set_schedule(TaskManager::TaskId(argv[2]), argv[3], argv[4],
757 runtime.databases));
758 }
759 if (command == "done" || command == "reopen" || command == "archive" || command == "delete") {
760 if (argc < 3) {
761 std::println(std::cerr, "{} requires <task_id>", command);
762 return static_cast<int>(ExitCode::usage);
763 }
764 if (command == "delete") {
765 return print_task_result(
766 command, TaskManager::delete_task(TaskManager::TaskId(argv[2]), runtime.databases));
767 }
768 if (command == "archive") {
769 return print_task_result(
770 command, TaskManager::archive_task(TaskManager::TaskId(argv[2]), runtime.databases));
771 }
772 return print_task_result(
773 command, TaskManager::set_task_done(TaskManager::TaskId(argv[2]), command == "done",
774 runtime.databases));
775 }
776 if (command == "timer-start" || command == "timer-stop") {
777 if (argc < 3) {
778 std::println(std::cerr, "{} requires <task_id>", command);
779 return static_cast<int>(ExitCode::usage);
780 }
781 if (command == "timer-start") {
782 return print_task_result(
783 command, TaskManager::start_timer(TaskManager::TaskId(argv[2]), runtime.databases));
784 }
785 return print_task_result(
786 command, TaskManager::stop_timer(TaskManager::TaskId(argv[2]), runtime.databases));
787 }
788 if (command == "timers") {
789 const auto timers = TaskManager::list_timers(runtime.databases);
790 if (!timers) {
791 std::println(std::cerr, "timers failed: {}",
792 TaskManager::task_error_message(timers.error()));
793 return static_cast<int>(ExitCode::failed);
794 }
795 for (const auto& timer : *timers) {
796 std::println("{}\t{}\t{}", timer.task_id, timer.started_at, timer.title);
797 }
798 return static_cast<int>(ExitCode::ok);
799 }
800 if (command == "sync-reminder") {
801 if (argc < 4) {
802 std::println(std::cerr, "sync-reminder requires <task_id> reminders|omnifocus|both");
803 return static_cast<int>(ExitCode::usage);
804 }
805 return run_sync_reminder(runtime, argv[2], argv[3],
806 has_option(arguments(argc, argv, 4), "--dry-run"));
807 }
808 if (command == "sync-week") {
809 if (argc < 3) {
810 std::println(std::cerr, "sync-week requires reminders|omnifocus|both");
811 return static_cast<int>(ExitCode::usage);
812 }
813 return run_sync_week(runtime, argv[2], has_option(arguments(argc, argv, 3), "--dry-run"));
814 }
815 if (command == "reminder-script") {
816 if (argc < 3 ||
817 (std::string_view(argv[2]) != "reminders" && std::string_view(argv[2]) != "omnifocus")) {
818 std::println(std::cerr, "reminder-script requires reminders|omnifocus");
819 return static_cast<int>(ExitCode::usage);
820 }
821 std::println("{}", std::string_view(argv[2]) == "reminders"
824 return static_cast<int>(ExitCode::ok);
825 }
826 if (command == "list") {
827 if (argc < 3) {
828 std::println(std::cerr, "list requires <project_file>");
829 return static_cast<int>(ExitCode::usage);
830 }
831 std::size_t count = 0;
832 const auto result = TaskManager::for_each_backlog_id(argv[2], [&count](std::string_view id) {
833 std::println("- {}", id);
834 ++count;
835 });
836 if (!result) {
837 std::println(std::cerr, "list failed: {}", TaskManager::task_error_message(result.error()));
838 return static_cast<int>(ExitCode::failed);
839 }
840 std::println("backlog tasks: {}", count);
841 return static_cast<int>(ExitCode::ok);
842 }
843
844 std::println(std::cerr, "unknown command: {}", command);
845 print_usage();
846 return static_cast<int>(ExitCode::usage);
847 } catch (const std::exception& error) {
848 // main 是最后的异常边界;把意外错误转换为稳定退出码 2。
849 std::println(std::cerr, "fatal: {}", error.what());
850 return static_cast<int>(ExitCode::failed);
851 }
852}
int main(int argc, char *argv[])
Definition main.cpp:642
接口输入/输出日志模块(STATIC 模块库)。
Stacks 2 项目/看板索引模块。
将带截止日期的 Stacks 任务同步到 Apple Reminders / OmniFocus。
Stacks 2 任务业务模块。
Alfred Script Filter JSON 与动作分发适配层。
std::expected< SyncSummary, SyncError > sync_all(const std::filesystem::path &search_path, const DbPaths &paths={})
同步新增路径与全部已记忆路径,并原子重建两个索引。
std::string_view sync_error_message(SyncError error) noexcept
SyncError 到稳定、可读的 CLI/Alfred 错误文本。
Provider
提醒事项提供方;both 表示两个目标都执行。
@ reminders
Apple Reminders。
@ both
Reminders + OmniFocus。
std::string build_reminders_script()
返回可在 osacompile 中独立编译的 Reminders AppleScript 源。
std::expected< SyncResult, SyncError > sync_task(const TaskReminder &task, Provider provider, const SyncOptions &options={})
将一个任务同步到指定外部应用。
std::string build_omnifocus_script()
返回可在 osacompile 中独立编译的 OmniFocus AppleScript 源。
std::string_view sync_error_message(SyncError error) noexcept
SyncError 到稳定可读文本。
std::expected< TaskRecord, TaskError > get_task(TaskId task_id, const DatabasePaths &paths)
以任务 ID 在工作区索引中定位并读取任务快照。
std::expected< std::vector< TaskRecord >, TaskError > list_week_due(const DatabasePaths &paths, std::optional< std::string > anchor_date=std::nullopt)
返回指定自然周内未完成、未归档且有截止日期的任务。
std::expected< std::size_t, TaskError > for_each_backlog_id(const std::filesystem::path &project_file, Visitor &&visitor)
以函数对象遍历项目 backlog ID。
std::expected< TaskResult, TaskError > create_inbox_task(TaskInput input, const CreateTaskOptions &options)
在指定/默认工作区创建 Stacks project=inbox 任务。
std::expected< TaskRecord, TaskError > set_due_date(TaskId task_id, std::string due_date, const DatabasePaths &paths)
仅更新截止日期;内部复用 update_task。
std::expected< TaskResult, TaskError > create_task(TargetId target_id, TaskInput input, const CreateTaskOptions &options)
在项目或清单下创建任务,并回滚“任务已写、项目未写”的半成功状态。
std::expected< std::vector< TimerRecord >, TaskError > list_timers(const DatabasePaths &paths)
列出 Timers.json 中的运行中计时快照。
std::expected< std::vector< BoardTarget >, TaskError > list_boards(const DatabasePaths &paths)
按项目名排序加载项目与清单的扁平列表。
std::expected< TaskRecord, TaskError > update_task(TaskId task_id, const TaskPatch &patch, const DatabasePaths &paths)
应用部分更新;修改前后都会验证日期区间与优先级。
std::expected< TaskRecord, TaskError > set_schedule(TaskId task_id, std::string start_date, std::string due_date, const DatabasePaths &paths)
同时更新开始与截止日期,并校验区间顺序。
std::expected< StopTimerResult, TaskError > stop_timer(TaskId task_id, const DatabasePaths &paths)
停止计时、写 Stacks timelog,并在第二步失败时回滚任务文件。
std::expected< DeleteResult, TaskError > delete_task(TaskId task_id, const DatabasePaths &paths)
从所有项目引用移除任务,并把 .task 移入可恢复 .trash。
std::expected< TaskRecord, TaskError > archive_task(TaskId task_id, const DatabasePaths &paths)
写入 archived 时间,保留任务文件供后续恢复。
std::expected< std::vector< TaskRecord >, TaskError > list_tasks(const DatabasePaths &paths, const TaskListOptions &options={})
扫描所有工作区任务并按目标、完成状态、归档状态和关键词过滤。
TaskError
任务业务的可恢复错误分类;UI 只消费 task_error_message。
std::expected< TaskRecord, TaskError > set_task_done(TaskId task_id, bool done, const DatabasePaths &paths)
写入 Stacks 完成字段或清空完成状态。
std::expected< TimerRecord, TaskError > start_timer(TaskId task_id, const DatabasePaths &paths)
在 Timers.json 中登记运行中计时;重复启动返回分类错误。
std::expected< TaskRecord, TaskError > set_start_date(TaskId task_id, std::string start_date, const DatabasePaths &paths)
仅更新开始日期;内部复用 update_task。
std::string_view task_error_message(TaskError error) noexcept
将 TaskError 映射为稳定的 CLI/Alfred 文本。
std::string filter(const Context &context, std::string_view query)
生成 Alfred Script Filter JSON;任何业务错误都转换为不可选错误条目。
std::expected< std::string, std::string > action(const Context &context, std::string_view token)
执行 Alfred action token;错误文本直接作为 Alfred Large Type/通知内容。
int run_sync_week(const Runtime &runtime, const std::string &provider_text, bool dry_run)
CLI sync-week:逐个同步本周截止任务。
Definition main.cpp:579
ReminderSync::Provider parse_provider_or_default(const std::string &value, bool *valid)
将 provider 文本解析为枚举;invalid 通过输出参数报告,避免抛异常。
Definition main.cpp:518
Runtime discover_runtime(const char *argv0)
发现运行根目录、workflow 目录和默认 Inbox。
Definition main.cpp:161
std::vector< std::string > arguments(int argc, char *argv[], int start)
从 argv[start..] 复制额外参数;start 之后的参数所有权仍属于进程。
Definition main.cpp:345
int run_action(const Runtime &runtime, const std::string &token)
CLI action:执行 Alfred 选中的 action token。
Definition main.cpp:630
int run_edit(const Runtime &runtime, const std::string &id, const std::vector< std::string > &values)
CLI edit:构造三态 TaskPatch 并写回任务。
Definition main.cpp:468
std::optional< std::string > ensure_runtime_layout(const fs::path &root)
首次运行补齐 data/log 和三个空 JSON。
Definition main.cpp:119
int run_inbox(const Runtime &runtime, const std::string &payload, const std::vector< std::string > &values)
CLI inbox:在默认/显式工作区创建收件箱任务。
Definition main.cpp:312
bool environment_flag(std::string_view name, bool fallback)
Alfred 布尔变量的宽容解析。
Definition main.cpp:97
int run_get(const Runtime &runtime, const std::string &id)
CLI get:输出单个任务的 Stacks 兼容字段。
Definition main.cpp:440
void print_usage()
打印完整 CLI 帮助;仅用于终端调试,不进入 Alfred Script Filter 输出。
Definition main.cpp:214
int run_week(const Runtime &runtime, std::optional< std::string > anchor)
CLI week:返回指定锚点周的未完成截止任务。
Definition main.cpp:422
int run_sync(const Runtime &runtime, const fs::path &path)
CLI sync:把新工作区交给 ProjectSync,并输出项目/清单统计。
Definition main.cpp:249
@ failed
可恢复业务/IO 失败。
Definition main.cpp:31
@ usage
参数不足或未知命令。
Definition main.cpp:30
void print_task(const TaskManager::TaskRecord &task)
任务列表行的稳定 TSV 格式,便于 awk/人工调试。
Definition main.cpp:242
int print_task_result(const std::string &command, const Result &result)
统一打印各种 expected 任务操作结果。
Definition main.cpp:493
std::expected< std::monostate, interface_log::LogError > initialize_log(const std::filesystem::path &path)
初始化 bqlog,并临时把 stdout 指向 stderr。
Definition main.cpp:55
bool has_option(const std::vector< std::string > &values, std::string_view option)
检查布尔开关参数是否存在,例如 --dry-run、--todo-only。
Definition main.cpp:365
int run_tasks(const Runtime &runtime, const std::vector< std::string > &values)
CLI tasks:把 –board/–query/–todo-only 映射为过滤条件。
Definition main.cpp:396
fs::path find_source_root(fs::path start)
源码运行时向上查找 CMakeLists.txt,作为项目根目录。
Definition main.cpp:81
std::optional< std::string > option_value(const std::vector< std::string > &values, std::string_view option)
返回第一个 --option value 的值;缺失返回 nullopt。
Definition main.cpp:354
int run_ui(const Runtime &runtime, const std::string &query)
CLI ui:把查询交给 WorkflowUI。
Definition main.cpp:619
int run_boards(const Runtime &runtime, std::string query)
CLI boards:按显示名/ID 过滤项目与清单。
Definition main.cpp:375
int run_add(const Runtime &runtime, const std::string &target, const std::string &payload, const std::vector< std::string > &values)
CLI add:向项目或清单创建任务。
Definition main.cpp:269
fs::path executable_directory(const char *argv0)
解析 argv[0] 的绝对目录;weakly_canonical 失败时回退到 absolute。
Definition main.cpp:74
int run_sync_reminder(const Runtime &runtime, const std::string &id, const std::string &provider_text, bool dry_run)
CLI sync-reminder:读取任务快照并同步到指定外部应用。
Definition main.cpp:541
void set_enabled(bool enabled)
启用/禁用接口记录(内存与文件同时生效)。
std::size_t cleanup_old_logs(const std::filesystem::path &log_path, int months_back=3)
删除超过保留期的接口日志与每日 trace 日志。
std::string_view log_error_message(LogError error) noexcept
LogError 到可读消息(供 CLI / 测试使用)。
void record(ApiName api, LogText input, LogText output, ApiStatus status)
记录一次接口调用。
std::expected< std::monostate, LogError > init(const std::filesystem::path &log_path="log/interface.log")
初始化 bqlog TextFileAppender(可重复调用;路径变化会重建后端)。
constexpr std::string_view kDeveloperWebsite
开发者网站水印;每次初始化 bqlog 与 CLI 调用日志都会写出。
外部应用同步选项。
bool dry_run
只验证,不调用 osascript。
与外部应用同步所需的最小任务快照。
三个运行数据库路径;一次传参避免同类 path 参数顺序错误。
std::filesystem::path stack_db
项目/清单扁平索引。
std::filesystem::path project_db
项目/工作区索引。
可恢复删除结果;trash_file 指向 tasks/.trash 中的副本。
停止计时结果;timelog_id 可在任务文件中定位新记录。
可创建任务的看板目标 ID。
任务描述;允许空字符串,不改写 Markdown/HTML 内容。
已校验任务 ID;作为所有任务读写接口的稳定主键。
创建任务输入;日期保留用户文本,由 normalize_date 在业务层校验。
任务列表的过滤条件;默认保留已完成任务、排除已归档任务。
std::optional< TargetId > target
项目、清单或特殊值 inbox。
std::string query
标题/描述/ID/项目/清单的模糊关键词。
bool include_done
是否显示已完成任务。
部分更新任务;optional 表示“未提供”,clear_* 表示“显式清空”。
bool clear_due_date
从 Stacks 文件移除 duedate。
std::optional< std::string > start_date
新开始日期表达式。
std::optional< std::string > title
新标题。
bool clear_start_date
从 Stacks 文件移除 startdate。
bool clear_priority
从 Stacks 文件移除 priority。
std::optional< std::string > description
新描述。
std::optional< std::string > priority
新优先级表达式。
std::optional< std::string > due_date
新截止日期表达式。
跨模块使用的任务只读快照;字段名对应 Stacks 文件与 UI 展示语义。
std::string title
任务标题。
std::string due_date
Stacks duedate。
bool done
Stacks done。
std::string board_title
清单显示名。
std::string id
任务 ID。
std::string project_title
项目显示名。
任务标题;create/update 时仍会执行 trim 与空值校验。
Timers.json 中的运行中计时快照。
单次 Alfred 调用所需的运行上下文。
一次进程调用共享的根目录、数据库路径和默认 Inbox 工作区。
Definition main.cpp:35
fs::path root
data/log 所在根目录。
Definition main.cpp:36
std::optional< fs::path > inbox_library
library_paths[0]。
Definition main.cpp:39
fs::path workflow_dir
Alfred workflow 根目录。
Definition main.cpp:37
TaskManager::DatabasePaths databases
三个运行 JSON。
Definition main.cpp:38
WorkflowUI::Context ui_context() const
把 CLI Runtime 映射为 WorkflowUI 的只读调用上下文。
Definition main.cpp:42
接口名强类型。 语义:调用方必须显式构造 ApiName,避免把任意字符串误当接口名。
接口输入/输出文本强类型。 语义:与 ApiName 一样显式构造,参数顺序在编译期被类型约束。