@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.
- package/LICENSE +201 -0
- package/README.md +141 -0
- package/bin/taskpilot +454 -0
- package/package.json +30 -0
- package/requirements.lock +66 -0
- package/src/taskpilot/__init__.py +1 -0
- package/src/taskpilot/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__init__.py +6 -0
- package/src/taskpilot/cli/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/app.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/app.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/context.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/context.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/errors.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/errors.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/exit_codes.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/exit_codes.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/output.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/output.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/registry.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/workspace.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/__pycache__/workspace.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/app.py +61 -0
- package/src/taskpilot/cli/commands/__init__.py +6 -0
- package/src/taskpilot/cli/commands/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/init.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/init.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/item.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/item.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/project.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/project.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/serve.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/serve.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/validate.cpython-311.pyc +0 -0
- package/src/taskpilot/cli/commands/__pycache__/validate.cpython-314.pyc +0 -0
- package/src/taskpilot/cli/commands/init.py +116 -0
- package/src/taskpilot/cli/commands/item.py +305 -0
- package/src/taskpilot/cli/commands/project.py +50 -0
- package/src/taskpilot/cli/commands/serve.py +78 -0
- package/src/taskpilot/cli/commands/validate.py +61 -0
- package/src/taskpilot/cli/context.py +36 -0
- package/src/taskpilot/cli/errors.py +53 -0
- package/src/taskpilot/cli/exit_codes.py +20 -0
- package/src/taskpilot/cli/output.py +77 -0
- package/src/taskpilot/cli/workspace.py +33 -0
- package/src/taskpilot/core/__init__.py +5 -0
- package/src/taskpilot/core/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/comments.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/comments.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/item_io.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/item_io.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/layout.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/layout.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/loader.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/models.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/models.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/project.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/project.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/timestamps.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/timestamps.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/validation.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/validation.cpython-314.pyc +0 -0
- package/src/taskpilot/core/__pycache__/yaml_io.cpython-311.pyc +0 -0
- package/src/taskpilot/core/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/src/taskpilot/core/comments.py +238 -0
- package/src/taskpilot/core/item_io.py +123 -0
- package/src/taskpilot/core/layout.py +137 -0
- package/src/taskpilot/core/loader.py +102 -0
- package/src/taskpilot/core/models.py +114 -0
- package/src/taskpilot/core/project.py +151 -0
- package/src/taskpilot/core/timestamps.py +54 -0
- package/src/taskpilot/core/validation.py +385 -0
- package/src/taskpilot/core/yaml_io.py +57 -0
- package/src/taskpilot/server/__init__.py +0 -0
- package/src/taskpilot/server/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/server/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/server/__pycache__/app.cpython-311.pyc +0 -0
- package/src/taskpilot/server/__pycache__/app.cpython-314.pyc +0 -0
- package/src/taskpilot/server/__pycache__/schemas.cpython-311.pyc +0 -0
- package/src/taskpilot/server/__pycache__/schemas.cpython-314.pyc +0 -0
- package/src/taskpilot/server/app.py +134 -0
- package/src/taskpilot/server/routes/__init__.py +0 -0
- package/src/taskpilot/server/routes/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/server/routes/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/server/routes/__pycache__/projects.cpython-311.pyc +0 -0
- package/src/taskpilot/server/routes/__pycache__/projects.cpython-314.pyc +0 -0
- package/src/taskpilot/server/routes/projects.py +160 -0
- package/src/taskpilot/server/schemas.py +76 -0
- package/src/taskpilot/services/__init__.py +8 -0
- package/src/taskpilot/services/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/comment_service.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/comment_service.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/errors.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/errors.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/hierarchy.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/hierarchy.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/item_service.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/item_service.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/link_service.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/link_service.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/operation_validation.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/operation_validation.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/project_service.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/project_service.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/registry.cpython-311.pyc +0 -0
- package/src/taskpilot/services/__pycache__/registry.cpython-314.pyc +0 -0
- package/src/taskpilot/services/__pycache__/reverse_links.cpython-314.pyc +0 -0
- package/src/taskpilot/services/comment_service.py +62 -0
- package/src/taskpilot/services/errors.py +26 -0
- package/src/taskpilot/services/hierarchy.py +107 -0
- package/src/taskpilot/services/item_service.py +264 -0
- package/src/taskpilot/services/link_service.py +97 -0
- package/src/taskpilot/services/operation_validation.py +52 -0
- package/src/taskpilot/services/project_service.py +111 -0
- package/src/taskpilot/services/registry.py +194 -0
- 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."""
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
"""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
"""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|