patchwork-os 0.2.0-alpha.0 → 0.2.0-alpha.2
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/README.md +41 -46
- package/dist/commands/dashboard.d.ts +47 -0
- package/dist/commands/dashboard.js +319 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/connectors/github.d.ts +44 -0
- package/dist/connectors/github.js +113 -0
- package/dist/connectors/github.js.map +1 -0
- package/dist/connectors/gmail.d.ts +40 -0
- package/dist/connectors/gmail.js +275 -0
- package/dist/connectors/gmail.js.map +1 -0
- package/dist/index.js +116 -22
- package/dist/index.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +95 -0
- package/dist/recipes/yamlRunner.js +533 -0
- package/dist/recipes/yamlRunner.js.map +1 -0
- package/dist/server.js +180 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/templates/recipes/gmail-health-check.yaml +19 -0
- package/templates/recipes/inbox-triage.yaml +15 -0
- package/templates/recipes/morning-brief.yaml +55 -0
- package/templates/scheduled-tasks/morning-brief/SKILL.md +37 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail OAuth 2.0 connector.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* GET /connections/gmail/auth — redirect to Google consent screen
|
|
6
|
+
* GET /connections/gmail/callback — exchange code for tokens, store locally
|
|
7
|
+
* POST /connections/gmail/test — verify stored token works
|
|
8
|
+
* DELETE /connections/gmail — revoke + delete stored token
|
|
9
|
+
* GET /connections — list connector statuses
|
|
10
|
+
*
|
|
11
|
+
* Tokens stored at ~/.patchwork/tokens/gmail.json (mode 0600, never leaves machine).
|
|
12
|
+
* Client credentials read from env: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET.
|
|
13
|
+
*/
|
|
14
|
+
export interface GmailTokens {
|
|
15
|
+
access_token: string;
|
|
16
|
+
refresh_token?: string;
|
|
17
|
+
expiry_date?: number;
|
|
18
|
+
token_type?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ConnectorStatus {
|
|
22
|
+
id: string;
|
|
23
|
+
status: "connected" | "disconnected";
|
|
24
|
+
lastSync?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function loadTokens(): GmailTokens | null;
|
|
28
|
+
/** Returns a valid access token, refreshing if needed. */
|
|
29
|
+
export declare function getValidAccessToken(): Promise<string>;
|
|
30
|
+
export interface ConnectorHandlerResult {
|
|
31
|
+
status: number;
|
|
32
|
+
body: string;
|
|
33
|
+
contentType?: string;
|
|
34
|
+
redirect?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function handleConnectionsList(): Promise<ConnectorHandlerResult>;
|
|
37
|
+
export declare function handleGmailAuthRedirect(): ConnectorHandlerResult;
|
|
38
|
+
export declare function handleGmailCallback(code: string | null, state: string | null, error: string | null): Promise<ConnectorHandlerResult>;
|
|
39
|
+
export declare function handleGmailTest(): Promise<ConnectorHandlerResult>;
|
|
40
|
+
export declare function handleGmailDisconnect(): Promise<ConnectorHandlerResult>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail OAuth 2.0 connector.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* GET /connections/gmail/auth — redirect to Google consent screen
|
|
6
|
+
* GET /connections/gmail/callback — exchange code for tokens, store locally
|
|
7
|
+
* POST /connections/gmail/test — verify stored token works
|
|
8
|
+
* DELETE /connections/gmail — revoke + delete stored token
|
|
9
|
+
* GET /connections — list connector statuses
|
|
10
|
+
*
|
|
11
|
+
* Tokens stored at ~/.patchwork/tokens/gmail.json (mode 0600, never leaves machine).
|
|
12
|
+
* Client credentials read from env: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET.
|
|
13
|
+
*/
|
|
14
|
+
import crypto from "node:crypto";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
const SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"];
|
|
19
|
+
const REDIRECT_URI = "http://localhost:3100/connections/gmail/callback";
|
|
20
|
+
const TOKEN_PATH = path.join(homedir(), ".patchwork", "tokens", "gmail.json");
|
|
21
|
+
function clientId() {
|
|
22
|
+
return process.env.GMAIL_CLIENT_ID ?? "";
|
|
23
|
+
}
|
|
24
|
+
function clientSecret() {
|
|
25
|
+
return process.env.GMAIL_CLIENT_SECRET ?? "";
|
|
26
|
+
}
|
|
27
|
+
function isConfigured() {
|
|
28
|
+
return Boolean(clientId() && clientSecret());
|
|
29
|
+
}
|
|
30
|
+
// ── Token storage ─────────────────────────────────────────────────────────────
|
|
31
|
+
export function loadTokens() {
|
|
32
|
+
if (!existsSync(TOKEN_PATH))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(TOKEN_PATH, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function saveTokens(tokens) {
|
|
42
|
+
mkdirSync(path.dirname(TOKEN_PATH), { recursive: true, mode: 0o700 });
|
|
43
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
function deleteTokens() {
|
|
46
|
+
if (existsSync(TOKEN_PATH))
|
|
47
|
+
unlinkSync(TOKEN_PATH);
|
|
48
|
+
}
|
|
49
|
+
// ── OAuth helpers ─────────────────────────────────────────────────────────────
|
|
50
|
+
function buildAuthUrl(state) {
|
|
51
|
+
const params = new URLSearchParams({
|
|
52
|
+
client_id: clientId(),
|
|
53
|
+
redirect_uri: REDIRECT_URI,
|
|
54
|
+
response_type: "code",
|
|
55
|
+
scope: SCOPES.join(" "),
|
|
56
|
+
access_type: "offline",
|
|
57
|
+
prompt: "consent",
|
|
58
|
+
state,
|
|
59
|
+
});
|
|
60
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
61
|
+
}
|
|
62
|
+
async function exchangeCode(code) {
|
|
63
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
66
|
+
body: new URLSearchParams({
|
|
67
|
+
code,
|
|
68
|
+
client_id: clientId(),
|
|
69
|
+
client_secret: clientSecret(),
|
|
70
|
+
redirect_uri: REDIRECT_URI,
|
|
71
|
+
grant_type: "authorization_code",
|
|
72
|
+
}).toString(),
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const body = await res.text();
|
|
76
|
+
throw new Error(`Token exchange failed: ${res.status} ${body}`);
|
|
77
|
+
}
|
|
78
|
+
const json = (await res.json());
|
|
79
|
+
return {
|
|
80
|
+
access_token: json.access_token,
|
|
81
|
+
refresh_token: json.refresh_token,
|
|
82
|
+
expiry_date: json.expires_in
|
|
83
|
+
? Date.now() + json.expires_in * 1000
|
|
84
|
+
: undefined,
|
|
85
|
+
token_type: json.token_type,
|
|
86
|
+
scope: json.scope,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function refreshAccessToken(tokens) {
|
|
90
|
+
if (!tokens.refresh_token)
|
|
91
|
+
throw new Error("No refresh token available");
|
|
92
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
95
|
+
body: new URLSearchParams({
|
|
96
|
+
refresh_token: tokens.refresh_token,
|
|
97
|
+
client_id: clientId(),
|
|
98
|
+
client_secret: clientSecret(),
|
|
99
|
+
grant_type: "refresh_token",
|
|
100
|
+
}).toString(),
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const body = await res.text();
|
|
104
|
+
throw new Error(`Token refresh failed: ${res.status} ${body}`);
|
|
105
|
+
}
|
|
106
|
+
const json = (await res.json());
|
|
107
|
+
const updated = {
|
|
108
|
+
...tokens,
|
|
109
|
+
access_token: json.access_token,
|
|
110
|
+
expiry_date: json.expires_in
|
|
111
|
+
? Date.now() + json.expires_in * 1000
|
|
112
|
+
: tokens.expiry_date,
|
|
113
|
+
};
|
|
114
|
+
saveTokens(updated);
|
|
115
|
+
return updated;
|
|
116
|
+
}
|
|
117
|
+
/** Returns a valid access token, refreshing if needed. */
|
|
118
|
+
export async function getValidAccessToken() {
|
|
119
|
+
let tokens = loadTokens();
|
|
120
|
+
if (!tokens)
|
|
121
|
+
throw new Error("Gmail not connected");
|
|
122
|
+
const bufferMs = 60_000;
|
|
123
|
+
if (tokens.expiry_date && Date.now() > tokens.expiry_date - bufferMs) {
|
|
124
|
+
tokens = await refreshAccessToken(tokens);
|
|
125
|
+
}
|
|
126
|
+
return tokens.access_token;
|
|
127
|
+
}
|
|
128
|
+
async function revokeToken(token) {
|
|
129
|
+
await fetch(`https://oauth2.googleapis.com/revoke?token=${encodeURIComponent(token)}`, { method: "POST" }).catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
async function fetchUserEmail(accessToken) {
|
|
132
|
+
const res = await fetch("https://gmail.googleapis.com/gmail/v1/users/me/profile", { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
133
|
+
if (!res.ok)
|
|
134
|
+
return "";
|
|
135
|
+
const json = (await res.json());
|
|
136
|
+
return json.emailAddress ?? "";
|
|
137
|
+
}
|
|
138
|
+
// ── State map (in-memory CSRF protection) ────────────────────────────────────
|
|
139
|
+
const pendingStates = new Set();
|
|
140
|
+
function generateState() {
|
|
141
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
142
|
+
pendingStates.add(state);
|
|
143
|
+
setTimeout(() => pendingStates.delete(state), 10 * 60 * 1000); // 10 min TTL
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
export async function handleConnectionsList() {
|
|
147
|
+
const tokens = loadTokens();
|
|
148
|
+
const { getStatus: getGitHubStatus } = await import("./github.js");
|
|
149
|
+
const gh = getGitHubStatus();
|
|
150
|
+
const connectors = [
|
|
151
|
+
{
|
|
152
|
+
id: "gmail",
|
|
153
|
+
status: tokens ? "connected" : "disconnected",
|
|
154
|
+
lastSync: tokens ? new Date().toISOString() : undefined,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "github",
|
|
158
|
+
status: gh.connected ? "connected" : "disconnected",
|
|
159
|
+
lastSync: gh.connected ? new Date().toISOString() : undefined,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
return {
|
|
163
|
+
status: 200,
|
|
164
|
+
contentType: "application/json",
|
|
165
|
+
body: JSON.stringify({ connectors }),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function handleGmailAuthRedirect() {
|
|
169
|
+
if (!isConfigured()) {
|
|
170
|
+
return {
|
|
171
|
+
status: 503,
|
|
172
|
+
contentType: "application/json",
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
error: "Gmail connector not configured. Set GMAIL_CLIENT_ID and GMAIL_CLIENT_SECRET.",
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const state = generateState();
|
|
179
|
+
const url = buildAuthUrl(state);
|
|
180
|
+
return { status: 302, redirect: url, body: "" };
|
|
181
|
+
}
|
|
182
|
+
export async function handleGmailCallback(code, state, error) {
|
|
183
|
+
if (error) {
|
|
184
|
+
return {
|
|
185
|
+
status: 400,
|
|
186
|
+
contentType: "text/html",
|
|
187
|
+
body: callbackHtml("Connection cancelled", `Google returned: ${error}`, false),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!code || !state || !pendingStates.has(state)) {
|
|
191
|
+
return {
|
|
192
|
+
status: 400,
|
|
193
|
+
contentType: "text/html",
|
|
194
|
+
body: callbackHtml("Invalid request", "Missing or expired state.", false),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
pendingStates.delete(state);
|
|
198
|
+
try {
|
|
199
|
+
const tokens = await exchangeCode(code);
|
|
200
|
+
saveTokens(tokens);
|
|
201
|
+
const email = await fetchUserEmail(tokens.access_token);
|
|
202
|
+
return {
|
|
203
|
+
status: 200,
|
|
204
|
+
contentType: "text/html",
|
|
205
|
+
body: callbackHtml("Gmail connected", `Connected${email ? ` as ${email}` : ""}. You can close this tab.`, true),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
return {
|
|
210
|
+
status: 500,
|
|
211
|
+
contentType: "text/html",
|
|
212
|
+
body: callbackHtml("Connection failed", err instanceof Error ? err.message : String(err), false),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function handleGmailTest() {
|
|
217
|
+
try {
|
|
218
|
+
const token = await getValidAccessToken();
|
|
219
|
+
const email = await fetchUserEmail(token);
|
|
220
|
+
return {
|
|
221
|
+
status: 200,
|
|
222
|
+
contentType: "application/json",
|
|
223
|
+
body: JSON.stringify({ ok: true, email }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return {
|
|
228
|
+
status: 400,
|
|
229
|
+
contentType: "application/json",
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
ok: false,
|
|
232
|
+
error: err instanceof Error ? err.message : String(err),
|
|
233
|
+
}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export async function handleGmailDisconnect() {
|
|
238
|
+
const tokens = loadTokens();
|
|
239
|
+
if (tokens?.access_token) {
|
|
240
|
+
await revokeToken(tokens.access_token);
|
|
241
|
+
}
|
|
242
|
+
deleteTokens();
|
|
243
|
+
return {
|
|
244
|
+
status: 200,
|
|
245
|
+
contentType: "application/json",
|
|
246
|
+
body: JSON.stringify({ ok: true }),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ── Callback HTML ─────────────────────────────────────────────────────────────
|
|
250
|
+
function callbackHtml(title, message, success) {
|
|
251
|
+
const color = success ? "#b8ff57" : "#ff5555";
|
|
252
|
+
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
|
253
|
+
<title>${title} — Patchwork OS</title>
|
|
254
|
+
<style>
|
|
255
|
+
body { background: #040406; color: #e0e0e0; font-family: system-ui, sans-serif;
|
|
256
|
+
display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
257
|
+
.card { background: #0d0d14; border: 1px solid #1e1e2e; border-radius: 8px;
|
|
258
|
+
padding: 40px 48px; max-width: 420px; text-align: center; }
|
|
259
|
+
h1 { color: ${color}; font-size: 1.4rem; margin-bottom: 12px; }
|
|
260
|
+
p { color: #888; line-height: 1.6; }
|
|
261
|
+
a { color: ${color}; text-decoration: none; font-size: 0.9rem; }
|
|
262
|
+
</style>
|
|
263
|
+
</head>
|
|
264
|
+
<body><div class="card">
|
|
265
|
+
<h1>${title}</h1>
|
|
266
|
+
<p>${message}</p>
|
|
267
|
+
<br><a href="javascript:window.close()">Close this tab</a>
|
|
268
|
+
</div>
|
|
269
|
+
<script>
|
|
270
|
+
// Notify the opener tab that auth completed so it can poll.
|
|
271
|
+
if (window.opener) { try { window.opener.postMessage('patchwork:gmail:connected', 'http://localhost:3100'); } catch(_) {} }
|
|
272
|
+
</script>
|
|
273
|
+
</body></html>`;
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=gmail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gmail.js","sourceRoot":"","sources":["../../src/connectors/gmail.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Load .env from repo root if present (connector credentials, etc.).
|
|
3
|
+
// Uses Node 20.6+ native dotenv loader; falls back to manual parse for older Node.
|
|
4
|
+
{
|
|
5
|
+
const { fileURLToPath: _fileURLToPath } = await import("node:url");
|
|
6
|
+
const envPath = _fileURLToPath(new URL("../../.env", import.meta.url));
|
|
7
|
+
try {
|
|
8
|
+
const { readFileSync, existsSync } = await import("node:fs");
|
|
9
|
+
if (existsSync(envPath)) {
|
|
10
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
11
|
+
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim());
|
|
12
|
+
if (m?.[1] && !process.env[m[1]]) {
|
|
13
|
+
process.env[m[1]] = m[2]?.replace(/^["']|["']$/g, "");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* non-fatal */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
2
22
|
// Enable V8 compile cache for faster cold-start on repeated restarts (Node 22.8+).
|
|
3
23
|
import nodeModule from "node:module";
|
|
4
24
|
if (typeof nodeModule.enableCompileCache === "function") {
|
|
@@ -547,39 +567,99 @@ export function register(ctx) {
|
|
|
547
567
|
process.stderr.write(` 3. Or add to your config: { "plugins": ["${outDir}"] }\n`);
|
|
548
568
|
process.exit(0);
|
|
549
569
|
}
|
|
550
|
-
// Patchwork: `patchwork recipe
|
|
551
|
-
|
|
570
|
+
// Patchwork: `patchwork recipe list` — enumerate installed recipes.
|
|
571
|
+
if (process.argv[2] === "recipe" && process.argv[3] === "list") {
|
|
572
|
+
(async () => {
|
|
573
|
+
const { listYamlRecipes } = await import("./recipes/yamlRunner.js");
|
|
574
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
575
|
+
const recipes = listYamlRecipes(recipesDir);
|
|
576
|
+
if (recipes.length === 0) {
|
|
577
|
+
process.stdout.write("No recipes installed. Run `patchwork-os patchwork-init` to install the starter set.\n");
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
process.stdout.write(`Installed recipes (${recipes.length}):\n\n`);
|
|
581
|
+
for (const r of recipes) {
|
|
582
|
+
const desc = r.description ? ` ${r.description}` : "";
|
|
583
|
+
process.stdout.write(` ${r.name.padEnd(28)} [${r.trigger}]${desc}\n`);
|
|
584
|
+
}
|
|
585
|
+
process.stdout.write(`\nRun a recipe: patchwork-os recipe run <name>\n`);
|
|
586
|
+
}
|
|
587
|
+
process.exit(0);
|
|
588
|
+
})();
|
|
589
|
+
}
|
|
590
|
+
// Patchwork: `patchwork recipe run <name>` — runs a recipe locally or via
|
|
591
|
+
// a running bridge's /recipes/run endpoint if one is available.
|
|
552
592
|
if (process.argv[2] === "recipe" && process.argv[3] === "run") {
|
|
553
|
-
const
|
|
593
|
+
const args = process.argv.slice(4);
|
|
594
|
+
const localFlag = args.includes("--local");
|
|
595
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
554
596
|
if (!name) {
|
|
555
|
-
process.stderr.write("Usage: patchwork recipe run <name
|
|
597
|
+
process.stderr.write("Usage: patchwork recipe run <name> [--local]\n");
|
|
556
598
|
process.exit(1);
|
|
557
599
|
}
|
|
558
600
|
(async () => {
|
|
559
601
|
try {
|
|
602
|
+
// Try bridge first (requires --claude-driver subprocess).
|
|
560
603
|
const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
|
|
561
|
-
const lock = findBridgeLock();
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
604
|
+
const lock = localFlag ? null : findBridgeLock();
|
|
605
|
+
if (lock) {
|
|
606
|
+
const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
|
|
607
|
+
method: "POST",
|
|
608
|
+
headers: {
|
|
609
|
+
Authorization: `Bearer ${lock.authToken}`,
|
|
610
|
+
"Content-Type": "application/json",
|
|
611
|
+
},
|
|
612
|
+
body: JSON.stringify({ name }),
|
|
613
|
+
});
|
|
614
|
+
const body = (await res.json());
|
|
615
|
+
if (!body.ok) {
|
|
616
|
+
// Fall through to local YAML runner if bridge doesn't know the recipe.
|
|
617
|
+
if (!(body.error ?? "").includes("not found")) {
|
|
618
|
+
process.stderr.write(`Error: ${body.error ?? "unknown"}\n`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// else: fall through to local runner below
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
process.stdout.write(` ✓ enqueued recipe "${name}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
|
|
626
|
+
" Watch progress on the dashboard Tasks page or via listClaudeTasks.\n");
|
|
627
|
+
process.exit(0);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
566
630
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
},
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
631
|
+
// No bridge — run locally using the YAML runner.
|
|
632
|
+
const { loadYamlRecipe, runYamlRecipe } = await import("./recipes/yamlRunner.js");
|
|
633
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
634
|
+
const bundledDir = fileURLToPath(new URL("../templates/recipes", import.meta.url));
|
|
635
|
+
const candidates = [
|
|
636
|
+
path.join(recipesDir, `${name}.yaml`),
|
|
637
|
+
path.join(recipesDir, `${name}.yml`),
|
|
638
|
+
path.join(recipesDir, `${name}.json`),
|
|
639
|
+
path.join(bundledDir, `${name}.yaml`),
|
|
640
|
+
path.join(bundledDir, `${name}.yml`),
|
|
641
|
+
];
|
|
642
|
+
let recipePath;
|
|
643
|
+
for (const c of candidates) {
|
|
644
|
+
if (existsSync(c)) {
|
|
645
|
+
recipePath = c;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (!recipePath) {
|
|
650
|
+
process.stderr.write(`Error: recipe "${name}" not found in ${recipesDir}\n` +
|
|
651
|
+
" Run `patchwork-os recipe list` to see available recipes.\n");
|
|
578
652
|
process.exit(1);
|
|
579
653
|
return;
|
|
580
654
|
}
|
|
581
|
-
process.stdout.write(`
|
|
582
|
-
|
|
655
|
+
process.stdout.write(` Running recipe "${name}" locally…\n`);
|
|
656
|
+
const recipe = loadYamlRecipe(recipePath);
|
|
657
|
+
const workdir = lock?.workspace || process.cwd();
|
|
658
|
+
const result = await runYamlRecipe(recipe, { workdir });
|
|
659
|
+
process.stdout.write(` ✓ ${result.stepsRun} step(s) completed\n`);
|
|
660
|
+
if (result.outputs.length > 0) {
|
|
661
|
+
process.stdout.write(` Output written to:\n${result.outputs.map((o) => ` ${o}`).join("\n")}\n`);
|
|
662
|
+
}
|
|
583
663
|
process.exit(0);
|
|
584
664
|
}
|
|
585
665
|
catch (err) {
|
|
@@ -1304,6 +1384,19 @@ Options:
|
|
|
1304
1384
|
process.exit(0);
|
|
1305
1385
|
}
|
|
1306
1386
|
// F6: "Did you mean?" for unknown CLI subcommands
|
|
1387
|
+
// Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
|
|
1388
|
+
{
|
|
1389
|
+
const binName = path.basename(process.argv[1] ?? "");
|
|
1390
|
+
const isPatchworkBin = binName === "patchwork-os" ||
|
|
1391
|
+
binName === "patchwork" ||
|
|
1392
|
+
binName === "patchwork.js";
|
|
1393
|
+
if (isPatchworkBin && !process.argv[2]) {
|
|
1394
|
+
(async () => {
|
|
1395
|
+
const { runDashboard } = await import("./commands/dashboard.js");
|
|
1396
|
+
await runDashboard();
|
|
1397
|
+
})();
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1307
1400
|
{
|
|
1308
1401
|
const KNOWN_COMMANDS = [
|
|
1309
1402
|
"init",
|
|
@@ -1319,6 +1412,7 @@ Options:
|
|
|
1319
1412
|
"status",
|
|
1320
1413
|
"shim",
|
|
1321
1414
|
"recipe",
|
|
1415
|
+
"dashboard",
|
|
1322
1416
|
];
|
|
1323
1417
|
const unknownSub = process.argv[2];
|
|
1324
1418
|
if (unknownSub &&
|