@alexey_platkovsky/taskpilot 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +141 -0
  3. package/bin/taskpilot +454 -0
  4. package/package.json +30 -0
  5. package/requirements.lock +66 -0
  6. package/src/taskpilot/__init__.py +1 -0
  7. package/src/taskpilot/__pycache__/__init__.cpython-311.pyc +0 -0
  8. package/src/taskpilot/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/src/taskpilot/cli/__init__.py +6 -0
  10. package/src/taskpilot/cli/__pycache__/__init__.cpython-311.pyc +0 -0
  11. package/src/taskpilot/cli/__pycache__/__init__.cpython-314.pyc +0 -0
  12. package/src/taskpilot/cli/__pycache__/app.cpython-311.pyc +0 -0
  13. package/src/taskpilot/cli/__pycache__/app.cpython-314.pyc +0 -0
  14. package/src/taskpilot/cli/__pycache__/context.cpython-311.pyc +0 -0
  15. package/src/taskpilot/cli/__pycache__/context.cpython-314.pyc +0 -0
  16. package/src/taskpilot/cli/__pycache__/errors.cpython-311.pyc +0 -0
  17. package/src/taskpilot/cli/__pycache__/errors.cpython-314.pyc +0 -0
  18. package/src/taskpilot/cli/__pycache__/exit_codes.cpython-311.pyc +0 -0
  19. package/src/taskpilot/cli/__pycache__/exit_codes.cpython-314.pyc +0 -0
  20. package/src/taskpilot/cli/__pycache__/output.cpython-311.pyc +0 -0
  21. package/src/taskpilot/cli/__pycache__/output.cpython-314.pyc +0 -0
  22. package/src/taskpilot/cli/__pycache__/registry.cpython-314.pyc +0 -0
  23. package/src/taskpilot/cli/__pycache__/workspace.cpython-311.pyc +0 -0
  24. package/src/taskpilot/cli/__pycache__/workspace.cpython-314.pyc +0 -0
  25. package/src/taskpilot/cli/app.py +61 -0
  26. package/src/taskpilot/cli/commands/__init__.py +6 -0
  27. package/src/taskpilot/cli/commands/__pycache__/__init__.cpython-311.pyc +0 -0
  28. package/src/taskpilot/cli/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  29. package/src/taskpilot/cli/commands/__pycache__/init.cpython-311.pyc +0 -0
  30. package/src/taskpilot/cli/commands/__pycache__/init.cpython-314.pyc +0 -0
  31. package/src/taskpilot/cli/commands/__pycache__/item.cpython-311.pyc +0 -0
  32. package/src/taskpilot/cli/commands/__pycache__/item.cpython-314.pyc +0 -0
  33. package/src/taskpilot/cli/commands/__pycache__/project.cpython-311.pyc +0 -0
  34. package/src/taskpilot/cli/commands/__pycache__/project.cpython-314.pyc +0 -0
  35. package/src/taskpilot/cli/commands/__pycache__/serve.cpython-311.pyc +0 -0
  36. package/src/taskpilot/cli/commands/__pycache__/serve.cpython-314.pyc +0 -0
  37. package/src/taskpilot/cli/commands/__pycache__/validate.cpython-311.pyc +0 -0
  38. package/src/taskpilot/cli/commands/__pycache__/validate.cpython-314.pyc +0 -0
  39. package/src/taskpilot/cli/commands/init.py +116 -0
  40. package/src/taskpilot/cli/commands/item.py +305 -0
  41. package/src/taskpilot/cli/commands/project.py +50 -0
  42. package/src/taskpilot/cli/commands/serve.py +78 -0
  43. package/src/taskpilot/cli/commands/validate.py +61 -0
  44. package/src/taskpilot/cli/context.py +36 -0
  45. package/src/taskpilot/cli/errors.py +53 -0
  46. package/src/taskpilot/cli/exit_codes.py +20 -0
  47. package/src/taskpilot/cli/output.py +77 -0
  48. package/src/taskpilot/cli/workspace.py +33 -0
  49. package/src/taskpilot/core/__init__.py +5 -0
  50. package/src/taskpilot/core/__pycache__/__init__.cpython-311.pyc +0 -0
  51. package/src/taskpilot/core/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/src/taskpilot/core/__pycache__/comments.cpython-311.pyc +0 -0
  53. package/src/taskpilot/core/__pycache__/comments.cpython-314.pyc +0 -0
  54. package/src/taskpilot/core/__pycache__/item_io.cpython-311.pyc +0 -0
  55. package/src/taskpilot/core/__pycache__/item_io.cpython-314.pyc +0 -0
  56. package/src/taskpilot/core/__pycache__/layout.cpython-311.pyc +0 -0
  57. package/src/taskpilot/core/__pycache__/layout.cpython-314.pyc +0 -0
  58. package/src/taskpilot/core/__pycache__/loader.cpython-314.pyc +0 -0
  59. package/src/taskpilot/core/__pycache__/models.cpython-311.pyc +0 -0
  60. package/src/taskpilot/core/__pycache__/models.cpython-314.pyc +0 -0
  61. package/src/taskpilot/core/__pycache__/project.cpython-311.pyc +0 -0
  62. package/src/taskpilot/core/__pycache__/project.cpython-314.pyc +0 -0
  63. package/src/taskpilot/core/__pycache__/timestamps.cpython-311.pyc +0 -0
  64. package/src/taskpilot/core/__pycache__/timestamps.cpython-314.pyc +0 -0
  65. package/src/taskpilot/core/__pycache__/validation.cpython-311.pyc +0 -0
  66. package/src/taskpilot/core/__pycache__/validation.cpython-314.pyc +0 -0
  67. package/src/taskpilot/core/__pycache__/yaml_io.cpython-311.pyc +0 -0
  68. package/src/taskpilot/core/__pycache__/yaml_io.cpython-314.pyc +0 -0
  69. package/src/taskpilot/core/comments.py +238 -0
  70. package/src/taskpilot/core/item_io.py +123 -0
  71. package/src/taskpilot/core/layout.py +137 -0
  72. package/src/taskpilot/core/loader.py +102 -0
  73. package/src/taskpilot/core/models.py +114 -0
  74. package/src/taskpilot/core/project.py +151 -0
  75. package/src/taskpilot/core/timestamps.py +54 -0
  76. package/src/taskpilot/core/validation.py +385 -0
  77. package/src/taskpilot/core/yaml_io.py +57 -0
  78. package/src/taskpilot/server/__init__.py +0 -0
  79. package/src/taskpilot/server/__pycache__/__init__.cpython-311.pyc +0 -0
  80. package/src/taskpilot/server/__pycache__/__init__.cpython-314.pyc +0 -0
  81. package/src/taskpilot/server/__pycache__/app.cpython-311.pyc +0 -0
  82. package/src/taskpilot/server/__pycache__/app.cpython-314.pyc +0 -0
  83. package/src/taskpilot/server/__pycache__/schemas.cpython-311.pyc +0 -0
  84. package/src/taskpilot/server/__pycache__/schemas.cpython-314.pyc +0 -0
  85. package/src/taskpilot/server/app.py +134 -0
  86. package/src/taskpilot/server/routes/__init__.py +0 -0
  87. package/src/taskpilot/server/routes/__pycache__/__init__.cpython-311.pyc +0 -0
  88. package/src/taskpilot/server/routes/__pycache__/__init__.cpython-314.pyc +0 -0
  89. package/src/taskpilot/server/routes/__pycache__/projects.cpython-311.pyc +0 -0
  90. package/src/taskpilot/server/routes/__pycache__/projects.cpython-314.pyc +0 -0
  91. package/src/taskpilot/server/routes/projects.py +160 -0
  92. package/src/taskpilot/server/schemas.py +76 -0
  93. package/src/taskpilot/services/__init__.py +8 -0
  94. package/src/taskpilot/services/__pycache__/__init__.cpython-311.pyc +0 -0
  95. package/src/taskpilot/services/__pycache__/__init__.cpython-314.pyc +0 -0
  96. package/src/taskpilot/services/__pycache__/comment_service.cpython-311.pyc +0 -0
  97. package/src/taskpilot/services/__pycache__/comment_service.cpython-314.pyc +0 -0
  98. package/src/taskpilot/services/__pycache__/errors.cpython-311.pyc +0 -0
  99. package/src/taskpilot/services/__pycache__/errors.cpython-314.pyc +0 -0
  100. package/src/taskpilot/services/__pycache__/hierarchy.cpython-311.pyc +0 -0
  101. package/src/taskpilot/services/__pycache__/hierarchy.cpython-314.pyc +0 -0
  102. package/src/taskpilot/services/__pycache__/item_service.cpython-311.pyc +0 -0
  103. package/src/taskpilot/services/__pycache__/item_service.cpython-314.pyc +0 -0
  104. package/src/taskpilot/services/__pycache__/link_service.cpython-311.pyc +0 -0
  105. package/src/taskpilot/services/__pycache__/link_service.cpython-314.pyc +0 -0
  106. package/src/taskpilot/services/__pycache__/operation_validation.cpython-311.pyc +0 -0
  107. package/src/taskpilot/services/__pycache__/operation_validation.cpython-314.pyc +0 -0
  108. package/src/taskpilot/services/__pycache__/project_service.cpython-311.pyc +0 -0
  109. package/src/taskpilot/services/__pycache__/project_service.cpython-314.pyc +0 -0
  110. package/src/taskpilot/services/__pycache__/registry.cpython-311.pyc +0 -0
  111. package/src/taskpilot/services/__pycache__/registry.cpython-314.pyc +0 -0
  112. package/src/taskpilot/services/__pycache__/reverse_links.cpython-314.pyc +0 -0
  113. package/src/taskpilot/services/comment_service.py +62 -0
  114. package/src/taskpilot/services/errors.py +26 -0
  115. package/src/taskpilot/services/hierarchy.py +107 -0
  116. package/src/taskpilot/services/item_service.py +264 -0
  117. package/src/taskpilot/services/link_service.py +97 -0
  118. package/src/taskpilot/services/operation_validation.py +52 -0
  119. package/src/taskpilot/services/project_service.py +111 -0
  120. package/src/taskpilot/services/registry.py +194 -0
  121. package/src/taskpilot/services/reverse_links.py +60 -0
package/bin/taskpilot ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * TaskPilot npm entry point.
5
+ *
6
+ * Discovers a compatible Python >=3.11 interpreter, manages a user-cache
7
+ * runtime environment, and delegates CLI commands to the bundled Python
8
+ * implementation.
9
+ *
10
+ * Handled inline (no delegation):
11
+ * taskpilot --version
12
+ * taskpilot doctor --rebuild-runtime
13
+ *
14
+ * All other commands are forwarded to Python with TASKPILOT_WEB_DIST set.
15
+ */
16
+
17
+ const { execSync, spawn } = require("child_process");
18
+ const fs = require("fs");
19
+ const os = require("os");
20
+ const path = require("path");
21
+
22
+ const packageJson = require("../package.json");
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // T3: Python discovery
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * @returns {string} The resolved Python command path.
30
+ */
31
+ function discoverPython() {
32
+ const override = process.env.TASKPILOT_PYTHON;
33
+ if (override) {
34
+ validatePython(override);
35
+ return override;
36
+ }
37
+
38
+ const candidates = process.platform === "win32"
39
+ ? ["python3", "python", "python3.14", "python3.13", "python3.12", "python3.11"]
40
+ : ["python3", "python", "python3.14", "python3.13", "python3.12", "python3.11"];
41
+ const errors = [];
42
+
43
+ for (const cmd of candidates) {
44
+ try {
45
+ validatePython(cmd, { fatal: false });
46
+ return cmd;
47
+ } catch (err) {
48
+ errors.push(err.message);
49
+ }
50
+ }
51
+
52
+ fail(
53
+ "Could not find a compatible Python interpreter.\n" +
54
+ "Install Python >=3.11 with venv and pip, or set TASKPILOT_PYTHON to\n" +
55
+ "the full path of a compatible Python executable." +
56
+ (errors.length ? "\nChecked candidates:\n- " + errors.join("\n- ") : "")
57
+ );
58
+ }
59
+
60
+ function validatePython(pythonCmd, options = {}) {
61
+ const fatal = options.fatal !== false;
62
+ function invalid(message) {
63
+ if (fatal) {
64
+ fail(message);
65
+ }
66
+ throw new Error(message);
67
+ }
68
+
69
+ let version;
70
+ try {
71
+ version = execSync(`"${pythonCmd}" -c "import sys; print(sys.version)"`, {
72
+ encoding: "utf8",
73
+ stdio: ["ignore", "pipe", "pipe"],
74
+ }).trim();
75
+ } catch {
76
+ invalid(`${pythonCmd}: failed to run Python. Check that it is a valid Python executable.`);
77
+ }
78
+
79
+ const match = version.match(/^(\d+)\.(\d+)/);
80
+ if (!match) {
81
+ invalid(`${pythonCmd}: could not determine Python version (output: ${version})`);
82
+ }
83
+
84
+ const major = parseInt(match[1], 10);
85
+ const minor = parseInt(match[2], 10);
86
+
87
+ if (major < 3 || (major === 3 && minor < 11)) {
88
+ invalid(
89
+ `${pythonCmd}: Python ${major}.${minor} is too old.\n` +
90
+ "TaskPilot requires Python >=3.11. Install a newer Python or set\n" +
91
+ "TASKPILOT_PYTHON to a compatible interpreter."
92
+ );
93
+ }
94
+
95
+ // Validate venv
96
+ try {
97
+ execSync(`"${pythonCmd}" -m venv --help`, {
98
+ encoding: "utf8",
99
+ stdio: ["ignore", "pipe", "pipe"],
100
+ });
101
+ } catch {
102
+ invalid(
103
+ `${pythonCmd}: missing venv module.\n` +
104
+ "Install the venv module for your Python distribution\n" +
105
+ "(e.g. 'python3 -m ensurepip && python3 -m pip install virtualenv'\n" +
106
+ "or install 'python3-venv' via your system package manager)."
107
+ );
108
+ }
109
+
110
+ // Validate pip (ensurepip bootstraps pip, check it's available)
111
+ try {
112
+ execSync(`"${pythonCmd}" -m pip --version`, {
113
+ encoding: "utf8",
114
+ stdio: ["ignore", "pipe", "pipe"],
115
+ });
116
+ } catch {
117
+ invalid(
118
+ `${pythonCmd}: missing pip.\n` +
119
+ "Run 'python3 -m ensurepip' or install pip for your Python distribution."
120
+ );
121
+ }
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // T4: Runtime cache
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Get the Python major.minor version string.
130
+ */
131
+ function getPythonVersion(pythonCmd) {
132
+ const output = execSync(
133
+ `"${pythonCmd}" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`,
134
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
135
+ ).trim();
136
+ return output;
137
+ }
138
+
139
+ /**
140
+ * Get the base cache directory for TaskPilot runtimes.
141
+ * Uses XDG/standard cache locations per platform.
142
+ */
143
+ function getCacheDir() {
144
+ if (process.platform === "win32") {
145
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
146
+ return path.join(localAppData, "taskpilot", "Cache");
147
+ }
148
+ if (process.platform === "darwin") {
149
+ return path.join(os.homedir(), "Library", "Caches", "taskpilot");
150
+ }
151
+ const xdg = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
152
+ return path.join(xdg, "taskpilot");
153
+ }
154
+
155
+ /**
156
+ * Get the versioned runtime path.
157
+ * Keyed by npm version and Python major.minor version.
158
+ */
159
+ function getRuntimePath(npmVersion, pythonVersion) {
160
+ return path.join(getCacheDir(), "runtimes", `npm-${npmVersion}`, `py${pythonVersion}`);
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // T5: Dependency installation
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Resolve the path to the Python executable inside a venv.
169
+ */
170
+ function venvPython(runtimePath) {
171
+ if (process.platform === "win32") {
172
+ return path.join(runtimePath, "Scripts", "python.exe");
173
+ }
174
+ return path.join(runtimePath, "bin", "python");
175
+ }
176
+
177
+ /**
178
+ * Ensure the runtime venv is ready.
179
+ * Returns the path to the venv Python executable.
180
+ */
181
+ function ensureRuntime(pythonCmd, runtimePath) {
182
+ const venvPy = venvPython(runtimePath);
183
+
184
+ if (fs.existsSync(venvPy)) {
185
+ // Quick health check: can the venv import a key dependency?
186
+ try {
187
+ execSync(`"${venvPy}" -c "import fastapi"`, {
188
+ encoding: "utf8",
189
+ stdio: ["ignore", "pipe", "pipe"],
190
+ });
191
+ process.stderr.write(`taskpilot: using cached runtime at ${runtimePath}\n`);
192
+ return venvPy;
193
+ } catch {
194
+ process.stderr.write("taskpilot: cached runtime is corrupted, rebuilding...\n");
195
+ deletePartialCache(runtimePath);
196
+ }
197
+ }
198
+
199
+ // --- acquire lock to prevent concurrent venv creation ---
200
+ const lockFile = runtimePath + ".lock";
201
+ try {
202
+ fs.mkdirSync(runtimePath, { recursive: true });
203
+ fs.mkdirSync(lockFile);
204
+ } catch (err) {
205
+ if (err.code === "EEXIST") {
206
+ if (!fs.existsSync(lockFile)) {
207
+ // runtimePath itself exists but lockFile doesn't — retry as top-level
208
+ return ensureRuntime(pythonCmd, runtimePath);
209
+ }
210
+ // Lock exists — poll until released (do NOT rmdir — it may belong to the owner)
211
+ process.stderr.write("taskpilot: waiting for another setup to finish...\n");
212
+ const deadline = Date.now() + 120000; // 2 minutes
213
+ while (fs.existsSync(lockFile)) {
214
+ if (Date.now() >= deadline) {
215
+ fail("Timed out waiting for runtime setup. Try again or run: taskpilot doctor --rebuild-runtime");
216
+ }
217
+ const spinEnd = Date.now() + 500;
218
+ while (Date.now() < spinEnd) { /* poll interval */ }
219
+ }
220
+ // Lock released — check if venv is now ready
221
+ if (fs.existsSync(venvPy)) {
222
+ try {
223
+ execSync(`"${venvPy}" -c "import fastapi"`, {
224
+ encoding: "utf8",
225
+ stdio: ["ignore", "pipe", "pipe"],
226
+ });
227
+ process.stderr.write(`taskpilot: using cached runtime at ${runtimePath}\n`);
228
+ return venvPy;
229
+ } catch {
230
+ // Corrupted — fall through to rebuild it
231
+ process.stderr.write("taskpilot: cached runtime is corrupted, rebuilding...\n");
232
+ deletePartialCache(runtimePath);
233
+ }
234
+ }
235
+ // Re-acquire lock and rebuild
236
+ try { fs.mkdirSync(lockFile); } catch { return ensureRuntime(pythonCmd, runtimePath); }
237
+ } else {
238
+ fail(`Failed to create runtime directory at ${runtimePath}: ${err.message}`);
239
+ }
240
+ }
241
+
242
+ // Release lock on exit/failure
243
+ function releaseLock() {
244
+ try { fs.rmdirSync(lockFile); } catch {}
245
+ }
246
+
247
+ process.stderr.write("taskpilot: setting up Python runtime (first run)...\n");
248
+
249
+ // --- create venv ---
250
+ process.stderr.write("taskpilot: creating virtual environment...\n");
251
+ try {
252
+ execSync(`"${pythonCmd}" -m venv "${runtimePath}"`, {
253
+ encoding: "utf8",
254
+ stdio: ["ignore", "pipe", "pipe"],
255
+ });
256
+ } catch (err) {
257
+ deletePartialCache(runtimePath);
258
+ releaseLock();
259
+ fail(
260
+ `Failed to create virtual environment at ${runtimePath}\n` +
261
+ `Cause: ${err.stderr || err.message}`
262
+ );
263
+ }
264
+
265
+ // --- install dependencies ---
266
+ process.stderr.write("taskpilot: installing dependencies...\n");
267
+
268
+ const packageDir = path.resolve(__dirname, "..");
269
+ const reqLock = path.join(packageDir, "requirements.lock");
270
+
271
+ if (!fs.existsSync(reqLock)) {
272
+ deletePartialCache(runtimePath);
273
+ releaseLock();
274
+ fail(`Bundled requirements.lock not found at ${reqLock}. Package may be incomplete.`);
275
+ }
276
+
277
+ const setupLog = path.join(runtimePath, "setup.log");
278
+
279
+ try {
280
+ execSync(
281
+ `"${venvPy}" -m pip install -r "${reqLock}" --quiet --no-input --log "${setupLog}"`,
282
+ {
283
+ encoding: "utf8",
284
+ stdio: ["ignore", "pipe", "pipe"],
285
+ timeout: 300000,
286
+ }
287
+ );
288
+ } catch (err) {
289
+ const failureLog = runtimePath + ".setup.log";
290
+ const tail = copySetupLogAndTail(setupLog, failureLog);
291
+ deletePartialCache(runtimePath);
292
+ releaseLock();
293
+
294
+ const isOffline =
295
+ (err.stderr || "").includes("Could not connect") ||
296
+ (err.stderr || "").includes("Failed to establish") ||
297
+ (err.stderr || "").includes("Name or service not known") ||
298
+ (err.stderr || "").includes("Network is unreachable") ||
299
+ tail.includes("Could not connect") ||
300
+ tail.includes("Failed to establish");
301
+
302
+ let instructions = "";
303
+ if (isOffline) {
304
+ instructions =
305
+ "\n\nSetup requires network access to install Python dependencies.\n" +
306
+ "Connect to the internet and try again.";
307
+ } else {
308
+ instructions =
309
+ "\n\nCheck the setup log for details: " + failureLog + "\n" +
310
+ "You can retry by running: taskpilot doctor --rebuild-runtime";
311
+ }
312
+
313
+ fail(
314
+ `Dependency installation failed.\n` +
315
+ `Setup log: ${failureLog}\n` +
316
+ `--- pip output (last lines) ---\n${tail}\n` +
317
+ `--- end pip output ---` +
318
+ instructions
319
+ );
320
+ }
321
+
322
+ releaseLock();
323
+ process.stderr.write("taskpilot: runtime ready\n");
324
+ return venvPy;
325
+ }
326
+
327
+ function copySetupLogAndTail(setupLog, failureLog) {
328
+ try {
329
+ fs.copyFileSync(setupLog, failureLog);
330
+ const logContent = fs.readFileSync(failureLog, "utf8");
331
+ const lines = logContent.trim().split("\n");
332
+ return lines.slice(-40).join("\n");
333
+ } catch {
334
+ return "(setup log unavailable)";
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Delete the partial runtime cache directory.
340
+ * Does not remove unrelated caches.
341
+ */
342
+ function deletePartialCache(runtimePath) {
343
+ if (fs.existsSync(runtimePath)) {
344
+ try {
345
+ fs.rmSync(runtimePath, { recursive: true, force: true });
346
+ process.stderr.write("taskpilot: removed partial cache\n");
347
+ } catch (err) {
348
+ process.stderr.write(`taskpilot: warning: could not remove partial cache at ${runtimePath}: ${err.message}\n`);
349
+ }
350
+ }
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // Command handling
355
+ // ---------------------------------------------------------------------------
356
+
357
+ function fail(message) {
358
+ process.stderr.write(`taskpilot: ${message}\n`);
359
+ process.exit(1);
360
+ }
361
+
362
+ function main() {
363
+ const args = process.argv.slice(2);
364
+
365
+ if (args.length === 1 && args[0] === "--version") {
366
+ process.stdout.write(`${packageJson.version}\n`);
367
+ process.exit(0);
368
+ }
369
+
370
+ if (args.length >= 1 && args[0] === "doctor" && args.includes("--rebuild-runtime")) {
371
+ const python = discoverPython();
372
+ const pyVersion = getPythonVersion(python);
373
+ const runtimePath = getRuntimePath(packageJson.version, pyVersion);
374
+
375
+ process.stderr.write(`taskpilot: rebuilding runtime for ${pyVersion}...\n`);
376
+ deletePartialCache(runtimePath);
377
+ ensureRuntime(python, runtimePath);
378
+ process.stderr.write("taskpilot: runtime rebuilt successfully\n");
379
+ process.exit(0);
380
+ }
381
+
382
+ // --- resolve python ---
383
+ const python = discoverPython();
384
+ const pyVersion = getPythonVersion(python);
385
+ const runtimePath = getRuntimePath(packageJson.version, pyVersion);
386
+
387
+ // --- ensure runtime is ready ---
388
+ const venvPy = ensureRuntime(python, runtimePath);
389
+
390
+ // --- resolve WebUI assets path ---
391
+ const packageDir = path.resolve(__dirname, "..");
392
+ const webDist = path.join(packageDir, "web-dist");
393
+
394
+ // --- delegate to Python ---
395
+ delegateToPython(venvPy, webDist, args);
396
+ }
397
+
398
+ // ---------------------------------------------------------------------------
399
+ // T7: Delegate to Python
400
+ // ---------------------------------------------------------------------------
401
+
402
+ /**
403
+ * Forward all arguments to the bundled Python implementation.
404
+ * Sets TASKPILOT_WEB_DIST to the staged WebUI assets directory.
405
+ * Forwards exit code and signals.
406
+ */
407
+ function delegateToPython(venvPy, webDist, args) {
408
+ const packageDir = path.resolve(__dirname, "..");
409
+ const srcDir = path.join(packageDir, "src");
410
+
411
+ const child = spawn(
412
+ venvPy,
413
+ ["-m", "taskpilot.cli.app", ...args],
414
+ {
415
+ stdio: "inherit",
416
+ env: {
417
+ ...process.env,
418
+ TASKPILOT_WEB_DIST: webDist,
419
+ PYTHONPATH: srcDir,
420
+ },
421
+ }
422
+ );
423
+
424
+ const signalHandlers = {};
425
+ const sigs = process.platform === "win32"
426
+ ? ["SIGINT", "SIGTERM"]
427
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
428
+
429
+ child.on("error", (err) => {
430
+ fail(`Failed to launch Python (${venvPy}): ${err.message}`);
431
+ });
432
+
433
+ child.on("close", (code, signal) => {
434
+ // Remove signal listeners
435
+ sigs.forEach((sig) => {
436
+ process.removeListener(sig, signalHandlers[sig]);
437
+ });
438
+
439
+ if (signal) {
440
+ // Child was killed by a signal; exit with convention 128+N
441
+ const sigNum = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 }[signal] || 1;
442
+ process.exit(128 + sigNum);
443
+ }
444
+ process.exit(code || 0);
445
+ });
446
+
447
+ sigs.forEach((sig) => {
448
+ const handler = () => child.kill(sig);
449
+ signalHandlers[sig] = handler;
450
+ process.on(sig, handler);
451
+ });
452
+ }
453
+
454
+ main();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@alexey_platkovsky/taskpilot",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Local-first, file-based task management. Markdown/YAML files are the source of truth.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "bin": {
7
+ "taskpilot": "bin/taskpilot"
8
+ },
9
+ "files": [
10
+ "bin/taskpilot",
11
+ "src/",
12
+ "web-dist/",
13
+ "requirements.lock",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "preflight": "bash scripts/release/preflight.sh",
19
+ "build:staging": "bash scripts/release/build-staging.sh",
20
+ "quality-gates": "bash scripts/release/quality-gates.sh",
21
+ "release:dry-run": "bash scripts/release/publish.sh --dry-run",
22
+ "release:publish": "bash scripts/release/publish.sh --publish",
23
+ "release": "npm run preflight && npm run quality-gates && npm run release:dry-run && npm run release:publish"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/anomalyco/task-pilot.git"
28
+ },
29
+ "private": false
30
+ }
@@ -0,0 +1,66 @@
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile pyproject.toml -o requirements.lock
3
+ annotated-doc==0.0.4
4
+ # via
5
+ # fastapi
6
+ # typer
7
+ annotated-types==0.7.0
8
+ # via pydantic
9
+ anyio==4.14.1
10
+ # via
11
+ # starlette
12
+ # watchfiles
13
+ click==8.4.2
14
+ # via uvicorn
15
+ fastapi==0.138.2
16
+ # via taskpilot (pyproject.toml)
17
+ h11==0.16.0
18
+ # via uvicorn
19
+ httptools==0.8.0
20
+ # via uvicorn
21
+ idna==3.18
22
+ # via anyio
23
+ markdown-it-py==4.2.0
24
+ # via rich
25
+ mdurl==0.1.2
26
+ # via markdown-it-py
27
+ pydantic==2.13.4
28
+ # via
29
+ # taskpilot (pyproject.toml)
30
+ # fastapi
31
+ pydantic-core==2.46.4
32
+ # via pydantic
33
+ pygments==2.20.0
34
+ # via rich
35
+ python-dotenv==1.2.2
36
+ # via uvicorn
37
+ pyyaml==6.0.3
38
+ # via
39
+ # taskpilot (pyproject.toml)
40
+ # uvicorn
41
+ rich==15.0.0
42
+ # via typer
43
+ shellingham==1.5.4
44
+ # via typer
45
+ starlette==1.3.1
46
+ # via fastapi
47
+ typer==0.26.8
48
+ # via taskpilot (pyproject.toml)
49
+ typing-extensions==4.15.0
50
+ # via
51
+ # fastapi
52
+ # pydantic
53
+ # pydantic-core
54
+ # typing-inspection
55
+ typing-inspection==0.4.2
56
+ # via
57
+ # fastapi
58
+ # pydantic
59
+ uvicorn==0.49.0
60
+ # via taskpilot (pyproject.toml)
61
+ uvloop==0.22.1 ; sys_platform != "win32"
62
+ # via uvicorn
63
+ watchfiles==1.2.0
64
+ # via uvicorn
65
+ websockets==16.0
66
+ # via uvicorn
@@ -0,0 +1 @@
1
+ """TaskPilot — local-first, file-based task management core."""
@@ -0,0 +1,6 @@
1
+ """TaskPilot CLI adapter (feature F003).
2
+
3
+ This package is an *adapter*: it translates command-line input into calls on the
4
+ F002 domain services and renders their results as human text or deterministic
5
+ JSON. It owns no domain rules — those live in :mod:`taskpilot.services`.
6
+ """
@@ -0,0 +1,61 @@
1
+ """TaskPilot CLI entry point and root Typer app (task F003-T1, requirement F003-R8).
2
+
3
+ Defines the root ``taskpilot`` command group and its global ``--json`` option.
4
+ Subcommands (``init``, ``project``, ``item``, ``validate``, ``serve``) are
5
+ registered onto :data:`app` by their own modules in later F003 tasks. The
6
+ console-script entry point in ``pyproject.toml`` points at :data:`app`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import typer
12
+
13
+ from taskpilot.cli.context import CLIState
14
+
15
+ __all__ = ["app"]
16
+
17
+ app = typer.Typer(
18
+ name="taskpilot",
19
+ help="TaskPilot — local-first, file-based task management.",
20
+ no_args_is_help=True,
21
+ add_completion=False,
22
+ )
23
+
24
+
25
+ @app.callback()
26
+ def main(
27
+ ctx: typer.Context,
28
+ json: bool = typer.Option(
29
+ False,
30
+ "--json",
31
+ help="Emit machine-readable JSON to stdout instead of human-readable text.",
32
+ ),
33
+ ) -> None:
34
+ """Resolve global options once and store them for every subcommand."""
35
+ ctx.obj = CLIState(json=json)
36
+
37
+
38
+ def _register_commands() -> None:
39
+ """Attach every command module to :data:`app`.
40
+
41
+ Imported here (not at module top) so command modules can import from this
42
+ module without a circular import.
43
+ """
44
+ from taskpilot.cli.commands import init as init_cmd
45
+ from taskpilot.cli.commands import item as item_cmd
46
+ from taskpilot.cli.commands import project as project_cmd
47
+ from taskpilot.cli.commands import serve as serve_cmd
48
+ from taskpilot.cli.commands import validate as validate_cmd
49
+
50
+ init_cmd.register(app)
51
+ project_cmd.register(app)
52
+ item_cmd.register(app)
53
+ validate_cmd.register(app)
54
+ serve_cmd.register(app)
55
+
56
+
57
+ _register_commands()
58
+
59
+
60
+ if __name__ == "__main__": # pragma: no cover - manual invocation convenience
61
+ app()
@@ -0,0 +1,6 @@
1
+ """Command modules for the TaskPilot CLI (feature F003).
2
+
3
+ Each module defines a command (or command group) and a ``register(app)`` hook;
4
+ :mod:`taskpilot.cli.app` calls every hook so commands attach without importing
5
+ the root app (avoiding a circular import).
6
+ """