@hasna/platform-todos-cli 0.1.0
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 +61 -0
- package/bin/platform-todos.js +2613 -0
- package/package.json +35 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,2613 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/index.ts
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
7
|
+
import { createInterface } from "readline/promises";
|
|
8
|
+
import { stderr as errorOutput, stdin as input, stdout as output } from "process";
|
|
9
|
+
|
|
10
|
+
// packages/cli/src/auth-store.ts
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
var DEFAULT_API_URL = "https://todos.md";
|
|
15
|
+
var AUTH_FILE = join(homedir(), ".platform-todos", "auth.json");
|
|
16
|
+
var PROFILE_DIR = join(homedir(), ".platform-todos", "profiles");
|
|
17
|
+
function normalizeApiUrl(value) {
|
|
18
|
+
if (!value?.trim())
|
|
19
|
+
return DEFAULT_API_URL;
|
|
20
|
+
const url = new URL(value.trim());
|
|
21
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
22
|
+
if (pathname === "/api" || pathname === "/api/v1") {
|
|
23
|
+
url.pathname = "/";
|
|
24
|
+
} else if (pathname.endsWith("/api/v1")) {
|
|
25
|
+
url.pathname = pathname.slice(0, -"/api/v1".length) || "/";
|
|
26
|
+
} else if (pathname.endsWith("/api")) {
|
|
27
|
+
url.pathname = pathname.slice(0, -"/api".length) || "/";
|
|
28
|
+
}
|
|
29
|
+
return url.toString().replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
function readAuthConfig(path = AUTH_FILE) {
|
|
32
|
+
if (!existsSync(path))
|
|
33
|
+
return null;
|
|
34
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
35
|
+
if (!parsed || typeof parsed !== "object")
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
apiUrl: normalizeApiUrl(parsed.apiUrl),
|
|
39
|
+
...typeof parsed.apiKey === "string" && parsed.apiKey ? { apiKey: parsed.apiKey } : {},
|
|
40
|
+
...typeof parsed.email === "string" && parsed.email ? { email: parsed.email } : {},
|
|
41
|
+
...typeof parsed.organizationId === "string" && parsed.organizationId ? { organizationId: parsed.organizationId } : {},
|
|
42
|
+
...typeof parsed.organizationSlug === "string" && parsed.organizationSlug ? { organizationSlug: parsed.organizationSlug } : {},
|
|
43
|
+
...typeof parsed.userId === "string" && parsed.userId ? { userId: parsed.userId } : {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function writeAuthConfig(config, path = AUTH_FILE) {
|
|
47
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
48
|
+
writeFileSync(path, `${JSON.stringify({ ...config, apiUrl: normalizeApiUrl(config.apiUrl) }, null, 2)}
|
|
49
|
+
`, {
|
|
50
|
+
mode: 384
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function clearAuthConfig(path = AUTH_FILE) {
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(path);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
function resolveAuthContext(input = {}) {
|
|
59
|
+
const authFile = input.authFile ?? authFileForProfile(input.profile ?? process.env.PLATFORM_TODOS_PROFILE ?? process.env.TODOS_PROFILE);
|
|
60
|
+
if (input.apiKey) {
|
|
61
|
+
return {
|
|
62
|
+
apiUrl: normalizeApiUrl(input.apiUrl ?? process.env.PLATFORM_TODOS_API_URL ?? process.env.TODOS_API_URL),
|
|
63
|
+
apiKey: input.apiKey,
|
|
64
|
+
source: "flag"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const envKey = process.env.PLATFORM_TODOS_API_KEY ?? process.env.TODOS_API_KEY;
|
|
68
|
+
if (envKey) {
|
|
69
|
+
return {
|
|
70
|
+
apiUrl: normalizeApiUrl(input.apiUrl ?? process.env.PLATFORM_TODOS_API_URL ?? process.env.TODOS_API_URL),
|
|
71
|
+
apiKey: envKey,
|
|
72
|
+
source: "env"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const stored = readAuthConfig(authFile);
|
|
76
|
+
if (stored?.apiKey) {
|
|
77
|
+
return {
|
|
78
|
+
apiUrl: normalizeApiUrl(input.apiUrl ?? stored.apiUrl),
|
|
79
|
+
apiKey: stored.apiKey,
|
|
80
|
+
source: "stored",
|
|
81
|
+
stored
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
apiUrl: normalizeApiUrl(input.apiUrl ?? process.env.PLATFORM_TODOS_API_URL ?? process.env.TODOS_API_URL),
|
|
86
|
+
source: "default",
|
|
87
|
+
...stored ? { stored } : {}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function redactSecret(value) {
|
|
91
|
+
if (!value)
|
|
92
|
+
return null;
|
|
93
|
+
if (value.length <= 10)
|
|
94
|
+
return "***";
|
|
95
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
96
|
+
}
|
|
97
|
+
function authFileForProfile(profile) {
|
|
98
|
+
const normalized = normalizeProfile(profile);
|
|
99
|
+
if (!normalized)
|
|
100
|
+
return AUTH_FILE;
|
|
101
|
+
return join(PROFILE_DIR, `${normalized}.json`);
|
|
102
|
+
}
|
|
103
|
+
function normalizeProfile(profile) {
|
|
104
|
+
const value = profile?.trim().toLowerCase();
|
|
105
|
+
if (!value || value === "default")
|
|
106
|
+
return null;
|
|
107
|
+
const normalized = value.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
108
|
+
return normalized || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// packages/cli/src/version.ts
|
|
112
|
+
var CLI_VERSION = "0.1.0";
|
|
113
|
+
var CLI_API_VERSION = "2026-05-10";
|
|
114
|
+
var CLI_USER_AGENT = `platform-todos/${CLI_VERSION}`;
|
|
115
|
+
|
|
116
|
+
// packages/cli/src/client.ts
|
|
117
|
+
class PlatformTodosApiError extends Error {
|
|
118
|
+
status;
|
|
119
|
+
body;
|
|
120
|
+
constructor(message, status, body) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.status = status;
|
|
123
|
+
this.body = body;
|
|
124
|
+
this.name = "PlatformTodosApiError";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
class PlatformTodosClient {
|
|
129
|
+
auth;
|
|
130
|
+
constructor(options = {}) {
|
|
131
|
+
this.auth = resolveAuthContext(options);
|
|
132
|
+
}
|
|
133
|
+
get apiUrl() {
|
|
134
|
+
return this.auth.apiUrl;
|
|
135
|
+
}
|
|
136
|
+
hasApiKey() {
|
|
137
|
+
return Boolean(this.auth.apiKey);
|
|
138
|
+
}
|
|
139
|
+
async login(email) {
|
|
140
|
+
return this.request("/api/auth/login", {
|
|
141
|
+
method: "POST",
|
|
142
|
+
body: { email },
|
|
143
|
+
requireAuth: false
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async verify(email, code) {
|
|
147
|
+
return this.request("/api/auth/verify", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
body: { email, code },
|
|
150
|
+
requireAuth: false
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async createApiKey(sessionToken, name = "platform-todos-cli", tenant) {
|
|
154
|
+
return this.request("/api/auth/keys", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
body: { name },
|
|
157
|
+
headers: {
|
|
158
|
+
...tenant?.organizationId ? { "x-organization-id": tenant.organizationId } : {},
|
|
159
|
+
...tenant?.email ? { "x-user-email": tenant.email } : {}
|
|
160
|
+
},
|
|
161
|
+
token: sessionToken,
|
|
162
|
+
requireAuth: true
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async listApiKeys() {
|
|
166
|
+
return this.request("/api/auth/keys");
|
|
167
|
+
}
|
|
168
|
+
async createManagedApiKey(input) {
|
|
169
|
+
const serviceAccountId = typeof input.serviceAccountId === "string" && input.serviceAccountId ? input.serviceAccountId : null;
|
|
170
|
+
return this.request(serviceAccountId ? `/api/v1/service-accounts/${encodeURIComponent(serviceAccountId)}/keys` : "/api/auth/keys", {
|
|
171
|
+
method: "POST",
|
|
172
|
+
body: input
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async revokeApiKey(id) {
|
|
176
|
+
return this.request(`/api/auth/keys/${encodeURIComponent(id)}/revoke`, { method: "POST" });
|
|
177
|
+
}
|
|
178
|
+
async rotateApiKey(id, name) {
|
|
179
|
+
return this.request(`/api/auth/keys/${encodeURIComponent(id)}/rotate`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: name ? { name } : {}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async listServiceAccounts() {
|
|
185
|
+
return this.request("/api/v1/service-accounts");
|
|
186
|
+
}
|
|
187
|
+
async createServiceAccount(input) {
|
|
188
|
+
return this.request("/api/v1/service-accounts", { method: "POST", body: input });
|
|
189
|
+
}
|
|
190
|
+
async whoami() {
|
|
191
|
+
return this.request("/api/auth/whoami");
|
|
192
|
+
}
|
|
193
|
+
async billingStatus() {
|
|
194
|
+
return this.request("/api/v1/billing/status");
|
|
195
|
+
}
|
|
196
|
+
async billingUsage() {
|
|
197
|
+
return this.request("/api/v1/billing/usage");
|
|
198
|
+
}
|
|
199
|
+
async billingCheckout(plan) {
|
|
200
|
+
const query = plan ? `?plan=${encodeURIComponent(plan)}` : "";
|
|
201
|
+
return this.request(`/api/v1/billing/checkout${query}`, { method: "POST" });
|
|
202
|
+
}
|
|
203
|
+
async billingPortal() {
|
|
204
|
+
return this.request("/api/v1/billing/portal", { method: "POST" });
|
|
205
|
+
}
|
|
206
|
+
async organizationSummary() {
|
|
207
|
+
return this.request("/api/v1/organization");
|
|
208
|
+
}
|
|
209
|
+
async updateOrganization(input) {
|
|
210
|
+
return this.request("/api/v1/organization", { method: "PATCH", body: input });
|
|
211
|
+
}
|
|
212
|
+
async createOrganizationInvitation(input) {
|
|
213
|
+
return this.request("/api/v1/organization/invitations", { method: "POST", body: input });
|
|
214
|
+
}
|
|
215
|
+
async acceptOrganizationInvitation(input) {
|
|
216
|
+
return this.request("/api/v1/organization/invitations/accept", { method: "POST", body: input });
|
|
217
|
+
}
|
|
218
|
+
async revokeOrganizationInvitation(invitationId) {
|
|
219
|
+
return this.request(`/api/v1/organization/invitations/${encodeURIComponent(invitationId)}/revoke`, { method: "POST" });
|
|
220
|
+
}
|
|
221
|
+
async changeOrganizationMemberRole(userId, role) {
|
|
222
|
+
return this.request(`/api/v1/organization/members/${encodeURIComponent(userId)}/role`, {
|
|
223
|
+
method: "PATCH",
|
|
224
|
+
body: { role }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async removeOrganizationMember(userId) {
|
|
228
|
+
return this.request(`/api/v1/organization/members/${encodeURIComponent(userId)}`, { method: "DELETE" });
|
|
229
|
+
}
|
|
230
|
+
async createAuditEvidenceExport(input, idempotencyKey) {
|
|
231
|
+
return this.request("/api/v1/audit/exports", {
|
|
232
|
+
method: "POST",
|
|
233
|
+
body: input,
|
|
234
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async createPrivacyExport(input, idempotencyKey) {
|
|
238
|
+
return this.request("/api/v1/privacy/exports", {
|
|
239
|
+
method: "POST",
|
|
240
|
+
body: input,
|
|
241
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async executePrivacyDeletion(input, idempotencyKey) {
|
|
245
|
+
return this.request("/api/v1/privacy/deletions", {
|
|
246
|
+
method: "POST",
|
|
247
|
+
body: input,
|
|
248
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async enforcePrivacyRetention(input, idempotencyKey) {
|
|
252
|
+
return this.request("/api/v1/privacy/retention", {
|
|
253
|
+
method: "POST",
|
|
254
|
+
body: input,
|
|
255
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async listTasks(query = "") {
|
|
259
|
+
return this.request(`/api/tasks${query}`);
|
|
260
|
+
}
|
|
261
|
+
async getTask(id) {
|
|
262
|
+
return this.request(`/api/tasks/${encodeURIComponent(id)}`);
|
|
263
|
+
}
|
|
264
|
+
async createTask(input) {
|
|
265
|
+
return this.request("/api/tasks", { method: "POST", body: input });
|
|
266
|
+
}
|
|
267
|
+
async startTask(id) {
|
|
268
|
+
return this.request(`/api/tasks/${encodeURIComponent(id)}/start`, { method: "POST" });
|
|
269
|
+
}
|
|
270
|
+
async completeTask(id) {
|
|
271
|
+
return this.request(`/api/tasks/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
272
|
+
}
|
|
273
|
+
async batchTasks(input, idempotencyKey) {
|
|
274
|
+
return this.request("/api/v1/tasks/batch", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
body: input,
|
|
277
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async search(input) {
|
|
281
|
+
return this.request("/api/v1/search", {
|
|
282
|
+
method: "POST",
|
|
283
|
+
body: input
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async getRunControls() {
|
|
287
|
+
return this.request("/api/v1/runs/controls");
|
|
288
|
+
}
|
|
289
|
+
async updateRunControls(input, idempotencyKey) {
|
|
290
|
+
return this.request("/api/v1/runs/controls", {
|
|
291
|
+
method: "POST",
|
|
292
|
+
body: input,
|
|
293
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async pauseRuns(input, idempotencyKey) {
|
|
297
|
+
return this.request("/api/v1/runs/controls/pause", {
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: input,
|
|
300
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async resumeRuns(idempotencyKey) {
|
|
304
|
+
return this.request("/api/v1/runs/controls/resume", {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async emergencyStopRuns(input, idempotencyKey) {
|
|
310
|
+
return this.request("/api/v1/runs/emergency-stop", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: input,
|
|
313
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async recordRunUsage(runId, input, idempotencyKey) {
|
|
317
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(runId)}/usage`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
body: input,
|
|
320
|
+
headers: idempotencyKey ? { "idempotency-key": idempotencyKey } : undefined
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async listRuns() {
|
|
324
|
+
return this.request("/api/v1/runs");
|
|
325
|
+
}
|
|
326
|
+
async createRun(slug, input) {
|
|
327
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(slug)}`, { method: "POST", body: input });
|
|
328
|
+
}
|
|
329
|
+
async getRun(id) {
|
|
330
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(id)}`);
|
|
331
|
+
}
|
|
332
|
+
async cancelRun(id) {
|
|
333
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
334
|
+
}
|
|
335
|
+
async listRunLogs(id) {
|
|
336
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(id)}/logs`);
|
|
337
|
+
}
|
|
338
|
+
async listRunArtifacts(id) {
|
|
339
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(id)}/artifacts`);
|
|
340
|
+
}
|
|
341
|
+
async listTaskDependencies(taskId) {
|
|
342
|
+
const query = taskId ? `?taskId=${encodeURIComponent(taskId)}` : "";
|
|
343
|
+
return this.request(`/api/v1/tasks/dependencies${query}`);
|
|
344
|
+
}
|
|
345
|
+
async createTaskDependency(input) {
|
|
346
|
+
return this.request("/api/v1/tasks/dependencies", { method: "POST", body: input });
|
|
347
|
+
}
|
|
348
|
+
async clearTaskDependency(id) {
|
|
349
|
+
return this.request(`/api/v1/tasks/dependencies/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
350
|
+
}
|
|
351
|
+
async readyTasks(input) {
|
|
352
|
+
const query = new URLSearchParams({ taskIds: input.taskIds.join(",") });
|
|
353
|
+
if (input.completedTaskIds?.length)
|
|
354
|
+
query.set("completedTaskIds", input.completedTaskIds.join(","));
|
|
355
|
+
if (input.claimedTaskIds?.length)
|
|
356
|
+
query.set("claimedTaskIds", input.claimedTaskIds.join(","));
|
|
357
|
+
return this.request(`/api/v1/tasks/ready?${query.toString()}`);
|
|
358
|
+
}
|
|
359
|
+
async nextReadyTask(input) {
|
|
360
|
+
return this.request("/api/v1/tasks/next", { method: "POST", body: input });
|
|
361
|
+
}
|
|
362
|
+
async claimTask(input = {}) {
|
|
363
|
+
return this.request("/api/tasks/claim", { method: "POST", body: input });
|
|
364
|
+
}
|
|
365
|
+
async listActivity(query = "") {
|
|
366
|
+
return this.request(`/api/v1/activity${query}`);
|
|
367
|
+
}
|
|
368
|
+
async recordActivityEvent(input) {
|
|
369
|
+
return this.request("/api/v1/activity/events", { method: "POST", body: input });
|
|
370
|
+
}
|
|
371
|
+
async listComments(query = "") {
|
|
372
|
+
return this.request(`/api/v1/comments${query}`);
|
|
373
|
+
}
|
|
374
|
+
async createComment(input) {
|
|
375
|
+
return this.request("/api/v1/comments", { method: "POST", body: input });
|
|
376
|
+
}
|
|
377
|
+
async listViews(query = "") {
|
|
378
|
+
return this.request(`/api/v1/views${query}`);
|
|
379
|
+
}
|
|
380
|
+
async createView(input) {
|
|
381
|
+
return this.request("/api/v1/views", { method: "POST", body: input });
|
|
382
|
+
}
|
|
383
|
+
async listViewItems(id, query = "") {
|
|
384
|
+
return this.request(`/api/v1/views/${encodeURIComponent(id)}/items${query}`);
|
|
385
|
+
}
|
|
386
|
+
async generateReport(kind, query = "") {
|
|
387
|
+
return this.request(`/api/v1/reports/${encodeURIComponent(kind)}${query}`);
|
|
388
|
+
}
|
|
389
|
+
async listPlanTemplates(organizationId) {
|
|
390
|
+
return this.request("/api/v1/plan-templates", {
|
|
391
|
+
headers: organizationId ? { "x-organization-id": organizationId } : {}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async createPlanFromTemplate(input, organizationId) {
|
|
395
|
+
return this.request("/api/v1/plans/from-template", {
|
|
396
|
+
method: "POST",
|
|
397
|
+
body: input,
|
|
398
|
+
headers: organizationId ? { "x-organization-id": organizationId } : {}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
async importLocalSqliteManifest(input) {
|
|
402
|
+
return this.request("/api/imports/local-sqlite", {
|
|
403
|
+
method: "POST",
|
|
404
|
+
body: {
|
|
405
|
+
mode: "copy-only",
|
|
406
|
+
conflictStrategy: input.conflictStrategy ?? "skip",
|
|
407
|
+
dryRun: input.dryRun ?? false,
|
|
408
|
+
manifest: input.manifest
|
|
409
|
+
},
|
|
410
|
+
headers: {
|
|
411
|
+
"x-organization-id": input.organizationId,
|
|
412
|
+
"idempotency-key": input.idempotencyKey
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async importLocalPlanMarkdown(input) {
|
|
417
|
+
return this.request("/api/imports/local-plan-markdown", {
|
|
418
|
+
method: "POST",
|
|
419
|
+
body: {
|
|
420
|
+
kind: "hasna.todos.local-plan-markdown.import",
|
|
421
|
+
schemaVersion: 1,
|
|
422
|
+
dryRun: input.dryRun ?? false,
|
|
423
|
+
artifacts: input.artifacts
|
|
424
|
+
},
|
|
425
|
+
headers: {
|
|
426
|
+
"x-organization-id": input.organizationId,
|
|
427
|
+
"idempotency-key": input.idempotencyKey
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async importExternalIssues(input) {
|
|
432
|
+
return this.request("/api/imports/issues", {
|
|
433
|
+
method: "POST",
|
|
434
|
+
body: input.body,
|
|
435
|
+
headers: {
|
|
436
|
+
"x-organization-id": input.organizationId
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async listApprovals(query = "") {
|
|
441
|
+
return this.request(`/api/v1/approvals${query}`);
|
|
442
|
+
}
|
|
443
|
+
async requestApproval(input) {
|
|
444
|
+
return this.request("/api/v1/approvals", { method: "POST", body: input });
|
|
445
|
+
}
|
|
446
|
+
async approveApproval(id, reason) {
|
|
447
|
+
return this.request(`/api/v1/approvals/${encodeURIComponent(id)}/approve`, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
body: reason ? { reason } : {}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async rejectApproval(id, reason) {
|
|
453
|
+
return this.request(`/api/v1/approvals/${encodeURIComponent(id)}/reject`, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
body: reason ? { reason } : {}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async expireApproval(id) {
|
|
459
|
+
return this.request(`/api/v1/approvals/${encodeURIComponent(id)}/expire`, { method: "POST" });
|
|
460
|
+
}
|
|
461
|
+
async listNotificationSubscriptions() {
|
|
462
|
+
return this.request("/api/v1/notifications/subscriptions");
|
|
463
|
+
}
|
|
464
|
+
async generatePlan(input) {
|
|
465
|
+
return this.request("/api/v1/plans/generate", {
|
|
466
|
+
method: "POST",
|
|
467
|
+
body: input.body,
|
|
468
|
+
headers: {
|
|
469
|
+
"x-organization-id": input.organizationId
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async listPlans(organizationId, query = "") {
|
|
474
|
+
return this.request(`/api/v1/plans${query}`, {
|
|
475
|
+
headers: { "x-organization-id": organizationId }
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async getPlan(organizationId, id) {
|
|
479
|
+
return this.request(`/api/v1/plans/${encodeURIComponent(id)}`, {
|
|
480
|
+
headers: { "x-organization-id": organizationId }
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
async updatePlan(organizationId, id, body) {
|
|
484
|
+
return this.request(`/api/v1/plans/${encodeURIComponent(id)}`, {
|
|
485
|
+
method: "PATCH",
|
|
486
|
+
body,
|
|
487
|
+
headers: { "x-organization-id": organizationId }
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async archivePlan(organizationId, id) {
|
|
491
|
+
return this.request(`/api/v1/plans/${encodeURIComponent(id)}/archive`, {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: { "x-organization-id": organizationId }
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
async listSandboxPolicies() {
|
|
497
|
+
return this.request("/api/v1/sandbox/policies");
|
|
498
|
+
}
|
|
499
|
+
async setSandboxPolicy(input) {
|
|
500
|
+
return this.request("/api/v1/sandbox/policies", { method: "PUT", body: input });
|
|
501
|
+
}
|
|
502
|
+
async deleteSandboxPolicy(input) {
|
|
503
|
+
return this.request("/api/v1/sandbox/policies", { method: "DELETE", body: input });
|
|
504
|
+
}
|
|
505
|
+
async createNotificationSubscription(input) {
|
|
506
|
+
return this.request("/api/v1/notifications/subscriptions", { method: "POST", body: input });
|
|
507
|
+
}
|
|
508
|
+
async disableNotificationSubscription(id) {
|
|
509
|
+
return this.request(`/api/v1/notifications/subscriptions/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
510
|
+
}
|
|
511
|
+
async listNotificationDeliveries() {
|
|
512
|
+
return this.request("/api/v1/notifications/deliveries");
|
|
513
|
+
}
|
|
514
|
+
async docsCatalog(surface, exposureProfile) {
|
|
515
|
+
const suffix = surface ? `/${encodeURIComponent(surface)}` : "";
|
|
516
|
+
const query = exposureProfile ? `?profile=${encodeURIComponent(exposureProfile)}` : "";
|
|
517
|
+
return this.request(`/api/v1/docs/catalog${suffix}${query}`, { requireAuth: false });
|
|
518
|
+
}
|
|
519
|
+
async request(path, options = {}) {
|
|
520
|
+
const requireAuth = options.requireAuth ?? true;
|
|
521
|
+
const token = options.token ?? this.auth.apiKey;
|
|
522
|
+
if (requireAuth && !token) {
|
|
523
|
+
throw new PlatformTodosApiError("Not signed in. Run: platform-todos auth login", 401, {
|
|
524
|
+
error: "auth_required"
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
let response;
|
|
528
|
+
try {
|
|
529
|
+
response = await fetch(`${this.apiUrl}${path}`, {
|
|
530
|
+
method: options.method ?? (options.body === undefined ? "GET" : "POST"),
|
|
531
|
+
headers: {
|
|
532
|
+
accept: "application/json",
|
|
533
|
+
"user-agent": CLI_USER_AGENT,
|
|
534
|
+
"x-platform-todos-api-version": CLI_API_VERSION,
|
|
535
|
+
"x-platform-todos-cli-version": CLI_VERSION,
|
|
536
|
+
...options.body === undefined ? {} : { "content-type": "application/json" },
|
|
537
|
+
...token ? { authorization: `Bearer ${token}` } : {},
|
|
538
|
+
...defaultTenantHeaders(this.auth),
|
|
539
|
+
...options.headers
|
|
540
|
+
},
|
|
541
|
+
...options.body === undefined ? {} : { body: JSON.stringify(options.body) }
|
|
542
|
+
});
|
|
543
|
+
} catch (error) {
|
|
544
|
+
throw new PlatformTodosApiError(`API unavailable at ${this.apiUrl}`, 503, {
|
|
545
|
+
error: "api_unavailable",
|
|
546
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const body = await readBody(response);
|
|
550
|
+
if (!response.ok) {
|
|
551
|
+
throw new PlatformTodosApiError(readErrorMessage(body, response.statusText), response.status, body);
|
|
552
|
+
}
|
|
553
|
+
return body;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function defaultTenantHeaders(auth) {
|
|
557
|
+
const organizationId = auth.stored?.organizationId ?? process.env.PLATFORM_TODOS_ORGANIZATION_ID ?? process.env.TODOS_ORGANIZATION_ID;
|
|
558
|
+
const email = auth.stored?.email ?? process.env.PLATFORM_TODOS_EMAIL ?? process.env.TODOS_EMAIL;
|
|
559
|
+
return {
|
|
560
|
+
...organizationId ? { "x-organization-id": organizationId } : {},
|
|
561
|
+
...email ? { "x-user-email": email } : {}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async function readBody(response) {
|
|
565
|
+
const text = await response.text();
|
|
566
|
+
if (!text)
|
|
567
|
+
return null;
|
|
568
|
+
try {
|
|
569
|
+
return JSON.parse(text);
|
|
570
|
+
} catch {
|
|
571
|
+
return text;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function readErrorMessage(body, fallback) {
|
|
575
|
+
if (body && typeof body === "object") {
|
|
576
|
+
const code = "error" in body ? String(body.error) : "";
|
|
577
|
+
const message = "message" in body ? String(body.message) : "";
|
|
578
|
+
if (code === "cli_version_unsupported" && message)
|
|
579
|
+
return message;
|
|
580
|
+
if (code === "unsupported_api_version" && message)
|
|
581
|
+
return message;
|
|
582
|
+
}
|
|
583
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
584
|
+
return String(body.error);
|
|
585
|
+
}
|
|
586
|
+
if (typeof body === "string" && body.trim())
|
|
587
|
+
return body;
|
|
588
|
+
return fallback || "Request failed";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// packages/cli/src/offline-queue.ts
|
|
592
|
+
import { createHash, randomUUID } from "crypto";
|
|
593
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
594
|
+
import { homedir as homedir2 } from "os";
|
|
595
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
596
|
+
function offlineQueueFileForProfile(profile) {
|
|
597
|
+
const normalized = normalizeProfile(profile);
|
|
598
|
+
if (!normalized)
|
|
599
|
+
return join2(homedir2(), ".platform-todos", "offline-queue.json");
|
|
600
|
+
return join2(homedir2(), ".platform-todos", "profiles", `${normalized}.offline-queue.json`);
|
|
601
|
+
}
|
|
602
|
+
function readOfflineQueue(path) {
|
|
603
|
+
if (!existsSync2(path))
|
|
604
|
+
return [];
|
|
605
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
606
|
+
if (!Array.isArray(parsed))
|
|
607
|
+
return [];
|
|
608
|
+
return parsed.filter(isOfflineQueueItem);
|
|
609
|
+
}
|
|
610
|
+
function writeOfflineQueue(path, items) {
|
|
611
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
612
|
+
writeFileSync2(path, `${JSON.stringify(items, null, 2)}
|
|
613
|
+
`, { mode: 384 });
|
|
614
|
+
}
|
|
615
|
+
function clearOfflineQueue(path) {
|
|
616
|
+
writeOfflineQueue(path, []);
|
|
617
|
+
}
|
|
618
|
+
function enqueueOfflineRequest(input) {
|
|
619
|
+
const queuePath = offlineQueueFileForProfile(input.profile);
|
|
620
|
+
const now = input.now ?? new Date;
|
|
621
|
+
const id = randomUUID();
|
|
622
|
+
const item = {
|
|
623
|
+
id,
|
|
624
|
+
operation: input.operation,
|
|
625
|
+
method: "POST",
|
|
626
|
+
path: input.path,
|
|
627
|
+
...input.body === undefined ? {} : { body: input.body },
|
|
628
|
+
idempotencyKey: input.idempotencyKey ?? stableIdempotencyKey(input.operation, input.path, input.body, id),
|
|
629
|
+
createdAt: now.toISOString(),
|
|
630
|
+
attempts: 0,
|
|
631
|
+
nextAttemptAt: now.toISOString(),
|
|
632
|
+
status: "pending"
|
|
633
|
+
};
|
|
634
|
+
writeOfflineQueue(queuePath, [...readOfflineQueue(queuePath), item]);
|
|
635
|
+
return item;
|
|
636
|
+
}
|
|
637
|
+
async function syncOfflineQueue(input) {
|
|
638
|
+
const queuePath = offlineQueueFileForProfile(input.profile);
|
|
639
|
+
const now = input.now ?? new Date;
|
|
640
|
+
const items = readOfflineQueue(queuePath);
|
|
641
|
+
const synced = [];
|
|
642
|
+
const retained = [];
|
|
643
|
+
const skipped = [];
|
|
644
|
+
for (const item of items) {
|
|
645
|
+
if (item.status !== "pending" || Date.parse(item.nextAttemptAt) > now.getTime()) {
|
|
646
|
+
skipped.push(item);
|
|
647
|
+
retained.push(item);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const response = await input.client.request(item.path, {
|
|
652
|
+
method: item.method,
|
|
653
|
+
body: item.body,
|
|
654
|
+
headers: { "Idempotency-Key": item.idempotencyKey }
|
|
655
|
+
});
|
|
656
|
+
synced.push({ id: item.id, operation: item.operation, response });
|
|
657
|
+
} catch (error) {
|
|
658
|
+
const retainedItem = withRetryState(item, error, now);
|
|
659
|
+
retained.push(retainedItem);
|
|
660
|
+
if (retainedItem.status !== "pending")
|
|
661
|
+
skipped.push(retainedItem);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
writeOfflineQueue(queuePath, retained);
|
|
665
|
+
return { synced, retained, skipped };
|
|
666
|
+
}
|
|
667
|
+
function withRetryState(item, error, now) {
|
|
668
|
+
const attempts = item.attempts + 1;
|
|
669
|
+
const status = error instanceof PlatformTodosApiError && error.status === 409 ? "conflict" : "pending";
|
|
670
|
+
const backoffSeconds = Math.min(3600, 2 ** Math.min(attempts, 10));
|
|
671
|
+
return {
|
|
672
|
+
...item,
|
|
673
|
+
attempts,
|
|
674
|
+
status,
|
|
675
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
676
|
+
nextAttemptAt: new Date(now.getTime() + backoffSeconds * 1000).toISOString()
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function stableIdempotencyKey(operation, path, body, id) {
|
|
680
|
+
const fingerprint = createHash("sha256").update(JSON.stringify({ operation, path, body, id })).digest("hex").slice(0, 24);
|
|
681
|
+
return `cli-offline-${fingerprint}`;
|
|
682
|
+
}
|
|
683
|
+
function isOfflineQueueItem(value) {
|
|
684
|
+
if (!value || typeof value !== "object")
|
|
685
|
+
return false;
|
|
686
|
+
const item = value;
|
|
687
|
+
return typeof item.id === "string" && typeof item.operation === "string" && item.method === "POST" && typeof item.path === "string" && typeof item.idempotencyKey === "string" && typeof item.createdAt === "string" && typeof item.nextAttemptAt === "string" && typeof item.attempts === "number" && (item.status === "pending" || item.status === "conflict");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// packages/cli/src/help.ts
|
|
691
|
+
var GLOBAL_OPTIONS = [
|
|
692
|
+
"--json",
|
|
693
|
+
"--api-url",
|
|
694
|
+
"--api-key",
|
|
695
|
+
"--profile",
|
|
696
|
+
"--non-interactive",
|
|
697
|
+
"--help",
|
|
698
|
+
"--version"
|
|
699
|
+
];
|
|
700
|
+
var PAID_CLI_COMMANDS = [
|
|
701
|
+
{
|
|
702
|
+
name: "auth",
|
|
703
|
+
usage: "auth login --email <email> [--code <code>] | auth logout | auth whoami | auth status",
|
|
704
|
+
summary: "Sign in, verify hosted credentials, inspect auth status, or remove stored credentials.",
|
|
705
|
+
auth: "optional",
|
|
706
|
+
examples: [
|
|
707
|
+
"platform-todos auth login --email you@example.com",
|
|
708
|
+
"platform-todos --json auth status",
|
|
709
|
+
"platform-todos --json auth whoami"
|
|
710
|
+
]
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "billing",
|
|
714
|
+
usage: "billing status | billing usage | billing checkout [--plan pro|team|storage] [--open] | billing portal [--open]",
|
|
715
|
+
summary: "Read hosted plan limits, usage meters, and create Checkout or Customer Portal sessions.",
|
|
716
|
+
auth: "required",
|
|
717
|
+
examples: [
|
|
718
|
+
"platform-todos --json billing status",
|
|
719
|
+
"platform-todos --json billing usage",
|
|
720
|
+
"platform-todos billing checkout --plan pro --open"
|
|
721
|
+
]
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
name: "api-keys",
|
|
725
|
+
usage: "api-keys list|create|select|revoke|rotate [--select] [--scopes <csv>]",
|
|
726
|
+
summary: "List key metadata, create or rotate scoped keys, select a key for the active profile, or revoke old keys.",
|
|
727
|
+
auth: "required",
|
|
728
|
+
examples: [
|
|
729
|
+
"platform-todos api-keys list",
|
|
730
|
+
"platform-todos api-keys create --name ci --scopes tasks:read,tasks:create --select",
|
|
731
|
+
"platform-todos api-keys select --key ak_example",
|
|
732
|
+
"platform-todos api-keys rotate <key-id> --select"
|
|
733
|
+
]
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "organization",
|
|
737
|
+
usage: "organization show|members|invitations|audit|update|invite|accept-invite|revoke-invite|role|remove",
|
|
738
|
+
summary: "Manage hosted organization settings, members, invitations, roles, and audit entries.",
|
|
739
|
+
auth: "required",
|
|
740
|
+
examples: [
|
|
741
|
+
"platform-todos organization members",
|
|
742
|
+
"platform-todos organization invite teammate@example.com --role member",
|
|
743
|
+
"platform-todos organization audit --json"
|
|
744
|
+
]
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "audit",
|
|
748
|
+
usage: "audit export --body <json>|--file <path> [--idempotency-key <key>]",
|
|
749
|
+
summary: "Create a redacted org-scoped audit and evidence export bundle.",
|
|
750
|
+
auth: "required",
|
|
751
|
+
examples: [
|
|
752
|
+
"platform-todos audit export --file evidence-request.json --idempotency-key audit-export-1"
|
|
753
|
+
]
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "privacy",
|
|
757
|
+
usage: "privacy <export|delete|retention> --body <json>|--file <path> [--idempotency-key <key>]",
|
|
758
|
+
summary: "Run admin-safe org privacy export, deletion, and retention controls.",
|
|
759
|
+
auth: "required",
|
|
760
|
+
examples: [
|
|
761
|
+
"platform-todos privacy export --body '{}' --idempotency-key privacy-export-1",
|
|
762
|
+
`platform-todos privacy delete --body '{"subjectEmail":"owner@example.com"}' --idempotency-key privacy-delete-1`,
|
|
763
|
+
"platform-todos privacy retention --body '{}' --idempotency-key privacy-retention-1"
|
|
764
|
+
]
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
name: "tasks",
|
|
768
|
+
usage: "tasks list|show|add|start|done|batch [--offline|--queue-offline|--dry-run]",
|
|
769
|
+
summary: "Use the hosted task API, queue task writes for later sync, or submit idempotent batch mutations.",
|
|
770
|
+
auth: "required",
|
|
771
|
+
examples: [
|
|
772
|
+
"platform-todos tasks list --status pending",
|
|
773
|
+
'platform-todos tasks add "Review launch" --idempotency-key launch-1',
|
|
774
|
+
"platform-todos tasks done task_123 --queue-offline",
|
|
775
|
+
`platform-todos tasks batch --body '{"operations":[{"type":"start_task","id":"task_123"}]}' --idempotency-key batch-1`
|
|
776
|
+
]
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: "activity",
|
|
780
|
+
usage: "activity list|event [--target-type <type>] [--target-id <id>]",
|
|
781
|
+
summary: "List tenant activity or record events on tasks, projects, plans, and runs.",
|
|
782
|
+
auth: "required",
|
|
783
|
+
examples: [
|
|
784
|
+
"platform-todos activity list --target-type task --target-id task_123",
|
|
785
|
+
"platform-todos activity event task.started --target-type task --target-id task_123"
|
|
786
|
+
]
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: "comments",
|
|
790
|
+
usage: "comments list|add [--target-type <type>] [--target-id <id>]",
|
|
791
|
+
summary: "List or add comments on tenant-scoped tasks, projects, plans, and runs.",
|
|
792
|
+
auth: "required",
|
|
793
|
+
examples: [
|
|
794
|
+
"platform-todos comments list --target-type task --target-id task_123",
|
|
795
|
+
'platform-todos comments add --target-type task --target-id task_123 --body "Reviewed"'
|
|
796
|
+
]
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: "views",
|
|
800
|
+
usage: "views list|create|items [options]",
|
|
801
|
+
summary: "Manage saved task, project, plan, and run views for agent workflows.",
|
|
802
|
+
auth: "required",
|
|
803
|
+
examples: [
|
|
804
|
+
"platform-todos views list --limit 20",
|
|
805
|
+
`platform-todos views create "Ready launches" --filter '{"types":["task"],"ready":true}'`,
|
|
806
|
+
"platform-todos views items ready --limit 10"
|
|
807
|
+
]
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: "reports",
|
|
811
|
+
usage: "reports summary|standup|sprint [--format json|markdown]",
|
|
812
|
+
summary: "Generate machine-readable or Markdown reports from saved views and filters.",
|
|
813
|
+
auth: "required",
|
|
814
|
+
examples: [
|
|
815
|
+
"platform-todos reports standup --format markdown",
|
|
816
|
+
"platform-todos reports summary --view ready --types task,run"
|
|
817
|
+
]
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "search",
|
|
821
|
+
usage: "search [query] [--type <types>] [--status <status>] [--limit <n>] [--body <json>|--file <path>]",
|
|
822
|
+
summary: "Search org-scoped hosted tasks, plans, runs, comments, imports, and audit artifacts.",
|
|
823
|
+
auth: "required",
|
|
824
|
+
examples: [
|
|
825
|
+
"platform-todos search deploy --type tasks,plans,runs --json",
|
|
826
|
+
`platform-todos search --body '{"query":"blocked","tasks":[{"id":"task-1","title":"Blocked deploy"}]}'`
|
|
827
|
+
]
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "runs",
|
|
831
|
+
usage: "runs controls|pause|resume|emergency-stop|usage",
|
|
832
|
+
summary: "Manage hosted run budgets, sandbox policies, queue pause/resume, emergency stops, and usage evidence.",
|
|
833
|
+
auth: "required",
|
|
834
|
+
examples: [
|
|
835
|
+
"platform-todos runs controls --budget-cents 5000 --max-run-cost-cents 500",
|
|
836
|
+
"platform-todos runs controls --agent-id codex --allow-tools shell,git --deny-tools aws --network-policy restricted",
|
|
837
|
+
"platform-todos runs pause --reason deploy-freeze",
|
|
838
|
+
"platform-todos runs emergency-stop --reason incident",
|
|
839
|
+
"platform-todos runs usage <run-id> --cost-cents 120 --tool-calls 8"
|
|
840
|
+
]
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "approvals",
|
|
844
|
+
usage: "approvals list|request|approve|reject|expire",
|
|
845
|
+
summary: "Manage manual approval gates for hosted agent runs.",
|
|
846
|
+
auth: "required",
|
|
847
|
+
examples: [
|
|
848
|
+
"platform-todos approvals list --status pending",
|
|
849
|
+
'platform-todos approvals request "before deploy" --run <run-id> --reviewer owner',
|
|
850
|
+
'platform-todos approvals approve <approval-id> --reason "reviewed"'
|
|
851
|
+
]
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: "sandbox",
|
|
855
|
+
usage: "sandbox policies|set|delete [--agent <id>]",
|
|
856
|
+
summary: "Manage organization and agent-specific execution policies for hosted runs.",
|
|
857
|
+
auth: "required",
|
|
858
|
+
examples: [
|
|
859
|
+
"platform-todos sandbox policies",
|
|
860
|
+
"platform-todos sandbox set --agent codex --allow-tools todos.add,todos.done --deny-tools shell.exec",
|
|
861
|
+
"platform-todos sandbox delete --agent codex"
|
|
862
|
+
]
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
name: "queue",
|
|
866
|
+
usage: "queue list|sync|clear",
|
|
867
|
+
summary: "Inspect, replay, or clear the local hosted-write offline queue.",
|
|
868
|
+
auth: "optional",
|
|
869
|
+
examples: [
|
|
870
|
+
"platform-todos queue list --json",
|
|
871
|
+
"platform-todos queue sync"
|
|
872
|
+
]
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
name: "import",
|
|
876
|
+
usage: "import local-sqlite|plan-markdown|issues [options]",
|
|
877
|
+
summary: "Copy explicit OSS local exports, import local plan Markdown artifacts, or preview/apply external issue imports.",
|
|
878
|
+
auth: "required",
|
|
879
|
+
examples: [
|
|
880
|
+
"platform-todos import local-sqlite --manifest .todos/export.json --org org_123 --idempotency-key import-1",
|
|
881
|
+
"platform-todos import plan-markdown --path .hasna/todos/plans/project_123 --org org_123 --idempotency-key plans-1",
|
|
882
|
+
"platform-todos import issues --source github --file issues.json --org org_123",
|
|
883
|
+
'platform-todos import issues --source markdown --raw "- [ ] Ship docs" --apply --org org_123'
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
name: "plans",
|
|
888
|
+
usage: "plans templates|list|show|update|archive|create|generate|refine [options]",
|
|
889
|
+
summary: "List reusable templates, inspect cloud plans, create plan drafts, or generate/update/refine agent-native plans.",
|
|
890
|
+
auth: "required",
|
|
891
|
+
examples: [
|
|
892
|
+
"platform-todos plans templates --org org_123",
|
|
893
|
+
"platform-todos plans list --org org_123",
|
|
894
|
+
"platform-todos plans show plan_123 --org org_123",
|
|
895
|
+
'platform-todos plans update plan_123 --feedback "Add owner review" --org org_123',
|
|
896
|
+
'platform-todos plans create --template release --title "Production release" --org org_123',
|
|
897
|
+
'platform-todos plans generate --objective "Create project. Add todos. Run agent." --org org_123',
|
|
898
|
+
'platform-todos plans refine --plan plan_123 --objective "Create project. Add todos. Run agent." --feedback "Add owner review" --org org_123'
|
|
899
|
+
]
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
name: "docs",
|
|
903
|
+
usage: "docs catalog [--surface <api|cli|mcp|sdk>] [--exposure-profile <profile>]",
|
|
904
|
+
summary: "Fetch machine-readable API, CLI, MCP, and SDK documentation from the hosted service.",
|
|
905
|
+
auth: "none",
|
|
906
|
+
examples: [
|
|
907
|
+
"platform-todos docs catalog --surface cli --json",
|
|
908
|
+
"platform-todos docs catalog --surface api --exposure-profile read-only"
|
|
909
|
+
]
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "api",
|
|
913
|
+
usage: "api get|post <path> [--body <json>]",
|
|
914
|
+
summary: "Call a hosted API endpoint directly when a wrapper command does not exist yet.",
|
|
915
|
+
auth: "required",
|
|
916
|
+
examples: [
|
|
917
|
+
"platform-todos api get /api/v1/capabilities --json",
|
|
918
|
+
`platform-todos api post /api/tasks --body '{"title":"Review"}'`
|
|
919
|
+
]
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
name: "config",
|
|
923
|
+
usage: "config show | config set [--api-url <url>] [--api-key <key>]",
|
|
924
|
+
summary: "Inspect or update local paid CLI API settings.",
|
|
925
|
+
auth: "none",
|
|
926
|
+
examples: [
|
|
927
|
+
"platform-todos config show --json",
|
|
928
|
+
"platform-todos --profile staging config set --api-url https://preview.todos.md --api-key ak_example"
|
|
929
|
+
]
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "completion",
|
|
933
|
+
usage: "completion <bash|zsh|fish>",
|
|
934
|
+
summary: "Generate shell completion scripts for the paid hosted CLI.",
|
|
935
|
+
auth: "none",
|
|
936
|
+
examples: [
|
|
937
|
+
"platform-todos completion bash > ~/.local/share/bash-completion/completions/platform-todos",
|
|
938
|
+
"platform-todos completion zsh > ~/.zfunc/_platform-todos",
|
|
939
|
+
"platform-todos completion fish > ~/.config/fish/completions/platform-todos.fish"
|
|
940
|
+
]
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
name: "manpage",
|
|
944
|
+
usage: "manpage",
|
|
945
|
+
summary: "Print a manpage-grade reference for the paid hosted CLI.",
|
|
946
|
+
auth: "none",
|
|
947
|
+
examples: [
|
|
948
|
+
"platform-todos manpage"
|
|
949
|
+
]
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: "help",
|
|
953
|
+
usage: "help [command]",
|
|
954
|
+
summary: "Print full help or help for one command group.",
|
|
955
|
+
auth: "none",
|
|
956
|
+
examples: [
|
|
957
|
+
"platform-todos help",
|
|
958
|
+
"platform-todos help tasks"
|
|
959
|
+
]
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
name: "version",
|
|
963
|
+
usage: "version",
|
|
964
|
+
summary: "Print the paid CLI version.",
|
|
965
|
+
auth: "none",
|
|
966
|
+
examples: [
|
|
967
|
+
"platform-todos version"
|
|
968
|
+
]
|
|
969
|
+
}
|
|
970
|
+
];
|
|
971
|
+
function paidCliCommandNames() {
|
|
972
|
+
return PAID_CLI_COMMANDS.map((command) => command.name);
|
|
973
|
+
}
|
|
974
|
+
function renderHelp(topic) {
|
|
975
|
+
const command = topic ? findCommand(topic) : undefined;
|
|
976
|
+
if (topic && !command) {
|
|
977
|
+
return `Unknown help topic: ${topic}
|
|
978
|
+
|
|
979
|
+
${renderHelp()}`;
|
|
980
|
+
}
|
|
981
|
+
if (command) {
|
|
982
|
+
return [
|
|
983
|
+
`platform-todos ${command.usage}`,
|
|
984
|
+
"",
|
|
985
|
+
command.summary,
|
|
986
|
+
"",
|
|
987
|
+
`Auth: ${authDescription(command.auth)}`,
|
|
988
|
+
"",
|
|
989
|
+
"Examples:",
|
|
990
|
+
...command.examples.map((example) => ` ${example}`),
|
|
991
|
+
"",
|
|
992
|
+
jsonAndExitContract()
|
|
993
|
+
].join(`
|
|
994
|
+
`);
|
|
995
|
+
}
|
|
996
|
+
const commandRows = PAID_CLI_COMMANDS.filter((doc) => doc.name !== "version").map((doc) => ` ${doc.usage.padEnd(74)} ${doc.summary}`);
|
|
997
|
+
return [
|
|
998
|
+
`platform-todos ${CLI_VERSION}`,
|
|
999
|
+
"",
|
|
1000
|
+
"Remote-only paid CLI for todos.md.",
|
|
1001
|
+
"",
|
|
1002
|
+
"Usage:",
|
|
1003
|
+
" platform-todos [--json] [--api-url <url>] [--api-key <key>] [--profile <name>] <command>",
|
|
1004
|
+
"",
|
|
1005
|
+
"Global options:",
|
|
1006
|
+
" --json Print machine-readable JSON output.",
|
|
1007
|
+
" --api-url <url> Hosted API root. Defaults to https://todos.md.",
|
|
1008
|
+
" --api-key <key> Non-interactive hosted API key.",
|
|
1009
|
+
" --profile <name> Isolate stored credentials and offline queue.",
|
|
1010
|
+
" --non-interactive Never prompt for login input.",
|
|
1011
|
+
" --help, -h Print help.",
|
|
1012
|
+
" --version, -V Print version.",
|
|
1013
|
+
"",
|
|
1014
|
+
"Commands:",
|
|
1015
|
+
...commandRows,
|
|
1016
|
+
"",
|
|
1017
|
+
"Install and update:",
|
|
1018
|
+
" bun install -g @hasna/platform-todos-cli",
|
|
1019
|
+
" bun update -g @hasna/platform-todos-cli",
|
|
1020
|
+
"",
|
|
1021
|
+
"Shell completions:",
|
|
1022
|
+
" platform-todos completion bash",
|
|
1023
|
+
" platform-todos completion zsh",
|
|
1024
|
+
" platform-todos completion fish",
|
|
1025
|
+
"",
|
|
1026
|
+
"Hosted boundary:",
|
|
1027
|
+
" This paid CLI talks to the hosted API only. It does not initialize, import, or run the public local task runtime unless you explicitly submit an export manifest.",
|
|
1028
|
+
"",
|
|
1029
|
+
jsonAndExitContract(),
|
|
1030
|
+
"",
|
|
1031
|
+
`Defaults: API URL ${DEFAULT_API_URL}; API key env PLATFORM_TODOS_API_KEY; profile env PLATFORM_TODOS_PROFILE.`
|
|
1032
|
+
].join(`
|
|
1033
|
+
`);
|
|
1034
|
+
}
|
|
1035
|
+
function renderManpage() {
|
|
1036
|
+
const sections = PAID_CLI_COMMANDS.map((command) => [
|
|
1037
|
+
`### ${command.name}`,
|
|
1038
|
+
"",
|
|
1039
|
+
`Usage: platform-todos ${command.usage}`,
|
|
1040
|
+
"",
|
|
1041
|
+
command.summary,
|
|
1042
|
+
"",
|
|
1043
|
+
`Auth: ${authDescription(command.auth)}`,
|
|
1044
|
+
"",
|
|
1045
|
+
"Examples:",
|
|
1046
|
+
...command.examples.map((example) => `- \`${example}\``)
|
|
1047
|
+
].join(`
|
|
1048
|
+
`));
|
|
1049
|
+
return [
|
|
1050
|
+
"# platform-todos(1)",
|
|
1051
|
+
"",
|
|
1052
|
+
"Remote-only paid CLI for the todos.md hosted platform.",
|
|
1053
|
+
"",
|
|
1054
|
+
"## Synopsis",
|
|
1055
|
+
"",
|
|
1056
|
+
"`platform-todos [--json] [--api-url <url>] [--api-key <key>] [--profile <name>] <command>`",
|
|
1057
|
+
"",
|
|
1058
|
+
"## Description",
|
|
1059
|
+
"",
|
|
1060
|
+
"platform-todos controls the hosted todos.md API from terminals, CI, and agents. It is separate from the public local-first task CLI and does not read local task data except when you explicitly submit an export manifest with `import local-sqlite`.",
|
|
1061
|
+
"",
|
|
1062
|
+
"## Global Options",
|
|
1063
|
+
"",
|
|
1064
|
+
...GLOBAL_OPTIONS.map((option) => `- \`${option}\``),
|
|
1065
|
+
"",
|
|
1066
|
+
"## Commands",
|
|
1067
|
+
"",
|
|
1068
|
+
...sections,
|
|
1069
|
+
"",
|
|
1070
|
+
"## JSON Contract",
|
|
1071
|
+
"",
|
|
1072
|
+
"Use `--json` for stable machine-readable stdout. Errors are written to stderr as `{ error, status, body? }`.",
|
|
1073
|
+
"",
|
|
1074
|
+
"## Exit Codes",
|
|
1075
|
+
"",
|
|
1076
|
+
"- `0`: command succeeded.",
|
|
1077
|
+
"- `1`: usage, authentication, quota, conflict, or HTTP error.",
|
|
1078
|
+
"- Other non-zero values are reserved for future fatal local failures.",
|
|
1079
|
+
"",
|
|
1080
|
+
"## Install And Update",
|
|
1081
|
+
"",
|
|
1082
|
+
"- `bun install -g @hasna/platform-todos-cli`",
|
|
1083
|
+
"- `bun update -g @hasna/platform-todos-cli`",
|
|
1084
|
+
""
|
|
1085
|
+
].join(`
|
|
1086
|
+
`);
|
|
1087
|
+
}
|
|
1088
|
+
function renderCompletion(shell) {
|
|
1089
|
+
if (shell === "bash")
|
|
1090
|
+
return renderBashCompletion();
|
|
1091
|
+
if (shell === "zsh")
|
|
1092
|
+
return renderZshCompletion();
|
|
1093
|
+
return renderFishCompletion();
|
|
1094
|
+
}
|
|
1095
|
+
function isCompletionShell(value) {
|
|
1096
|
+
return value === "bash" || value === "zsh" || value === "fish";
|
|
1097
|
+
}
|
|
1098
|
+
function renderBashCompletion() {
|
|
1099
|
+
const commands = completionWords();
|
|
1100
|
+
return `# bash completion for platform-todos
|
|
1101
|
+
_platform_todos_completion() {
|
|
1102
|
+
local current
|
|
1103
|
+
current="\${COMP_WORDS[COMP_CWORD]}"
|
|
1104
|
+
COMPREPLY=( $(compgen -W "${commands}" -- "$current") )
|
|
1105
|
+
}
|
|
1106
|
+
complete -F _platform_todos_completion platform-todos todosmd
|
|
1107
|
+
`;
|
|
1108
|
+
}
|
|
1109
|
+
function renderZshCompletion() {
|
|
1110
|
+
return `#compdef platform-todos todosmd
|
|
1111
|
+
_platform_todos() {
|
|
1112
|
+
local -a commands
|
|
1113
|
+
commands=(
|
|
1114
|
+
${PAID_CLI_COMMANDS.map((command) => ` '${command.name}:${escapeZsh(command.summary)}'`).join(`
|
|
1115
|
+
`)}
|
|
1116
|
+
)
|
|
1117
|
+
_describe 'platform-todos command' commands
|
|
1118
|
+
}
|
|
1119
|
+
_platform_todos "$@"
|
|
1120
|
+
`;
|
|
1121
|
+
}
|
|
1122
|
+
function renderFishCompletion() {
|
|
1123
|
+
const commandLines = PAID_CLI_COMMANDS.map((command) => `complete -c platform-todos -f -a '${command.name}' -d '${escapeSingle(command.summary)}'`).join(`
|
|
1124
|
+
`);
|
|
1125
|
+
const optionLines = GLOBAL_OPTIONS.map((option) => `complete -c platform-todos -l ${option.replace(/^--/, "")}`).join(`
|
|
1126
|
+
`);
|
|
1127
|
+
return `# fish completion for platform-todos
|
|
1128
|
+
${commandLines}
|
|
1129
|
+
${optionLines}
|
|
1130
|
+
complete -c todosmd -w platform-todos
|
|
1131
|
+
`;
|
|
1132
|
+
}
|
|
1133
|
+
function findCommand(topic) {
|
|
1134
|
+
const normalized = topic === "org" || topic === "team" ? "organization" : topic === "completions" ? "completion" : topic;
|
|
1135
|
+
return PAID_CLI_COMMANDS.find((command) => command.name === normalized);
|
|
1136
|
+
}
|
|
1137
|
+
function completionWords() {
|
|
1138
|
+
return [...paidCliCommandNames(), ...GLOBAL_OPTIONS].join(" ");
|
|
1139
|
+
}
|
|
1140
|
+
function authDescription(auth) {
|
|
1141
|
+
if (auth === "required")
|
|
1142
|
+
return "requires a hosted API key or signed-in profile";
|
|
1143
|
+
if (auth === "optional")
|
|
1144
|
+
return "some subcommands require hosted auth";
|
|
1145
|
+
return "does not require hosted auth";
|
|
1146
|
+
}
|
|
1147
|
+
function jsonAndExitContract() {
|
|
1148
|
+
return [
|
|
1149
|
+
"JSON and exit codes:",
|
|
1150
|
+
" --json writes stable machine-readable stdout.",
|
|
1151
|
+
" Errors write JSON to stderr as { error, status, body? } when --json is set.",
|
|
1152
|
+
" Exit 0 means success; exit 1 means usage, auth, quota, conflict, or HTTP failure."
|
|
1153
|
+
].join(`
|
|
1154
|
+
`);
|
|
1155
|
+
}
|
|
1156
|
+
function escapeZsh(value) {
|
|
1157
|
+
return value.replace(/'/g, "'\\''");
|
|
1158
|
+
}
|
|
1159
|
+
function escapeSingle(value) {
|
|
1160
|
+
return value.replace(/'/g, "\\'");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// packages/cli/src/index.ts
|
|
1164
|
+
var VERSION = CLI_VERSION;
|
|
1165
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1166
|
+
const parsed = parseArgs(argv);
|
|
1167
|
+
const [group, subcommand, ...rest] = parsed.command;
|
|
1168
|
+
try {
|
|
1169
|
+
if (!group || group === "--help" || group === "-h") {
|
|
1170
|
+
printOutput(renderHelp(), parsed.globals);
|
|
1171
|
+
return 0;
|
|
1172
|
+
}
|
|
1173
|
+
if (group === "help") {
|
|
1174
|
+
printOutput(renderHelp(subcommand), parsed.globals);
|
|
1175
|
+
return 0;
|
|
1176
|
+
}
|
|
1177
|
+
if (group === "--version" || group === "-V" || group === "version") {
|
|
1178
|
+
printOutput(VERSION, parsed.globals);
|
|
1179
|
+
return 0;
|
|
1180
|
+
}
|
|
1181
|
+
if (group === "auth")
|
|
1182
|
+
return await handleAuth(subcommand, rest, parsed.globals);
|
|
1183
|
+
if (group === "api-keys" || group === "api-key")
|
|
1184
|
+
return await handleApiKeys(subcommand, rest, parsed.globals);
|
|
1185
|
+
if (group === "service-accounts" || group === "service-account")
|
|
1186
|
+
return await handleServiceAccounts(subcommand, rest, parsed.globals);
|
|
1187
|
+
if (group === "billing")
|
|
1188
|
+
return await handleBilling(subcommand, rest, parsed.globals);
|
|
1189
|
+
if (group === "organization" || group === "org" || group === "team")
|
|
1190
|
+
return await handleOrganization(subcommand, rest, parsed.globals);
|
|
1191
|
+
if (group === "audit")
|
|
1192
|
+
return await handleAudit(subcommand, rest, parsed.globals);
|
|
1193
|
+
if (group === "privacy")
|
|
1194
|
+
return await handlePrivacy(subcommand, rest, parsed.globals);
|
|
1195
|
+
if (group === "search" || group === "find")
|
|
1196
|
+
return await handleSearch(subcommand, rest, parsed.globals);
|
|
1197
|
+
if (group === "runs")
|
|
1198
|
+
return await handleRuns(subcommand, rest, parsed.globals);
|
|
1199
|
+
if (group === "tasks")
|
|
1200
|
+
return await handleTasks(subcommand, rest, parsed.globals);
|
|
1201
|
+
if (group === "notifications" || group === "webhooks")
|
|
1202
|
+
return await handleNotifications(subcommand, rest, parsed.globals);
|
|
1203
|
+
if (group === "approvals" || group === "approval")
|
|
1204
|
+
return await handleApprovals(subcommand, rest, parsed.globals);
|
|
1205
|
+
if (group === "activity")
|
|
1206
|
+
return await handleActivity(subcommand, rest, parsed.globals);
|
|
1207
|
+
if (group === "comments")
|
|
1208
|
+
return await handleComments(subcommand, rest, parsed.globals);
|
|
1209
|
+
if (group === "views")
|
|
1210
|
+
return await handleViews(subcommand, rest, parsed.globals);
|
|
1211
|
+
if (group === "reports")
|
|
1212
|
+
return await handleReports(subcommand, rest, parsed.globals);
|
|
1213
|
+
if (group === "import")
|
|
1214
|
+
return await handleImport(subcommand, rest, parsed.globals);
|
|
1215
|
+
if (group === "sandbox")
|
|
1216
|
+
return await handleSandbox(subcommand, rest, parsed.globals);
|
|
1217
|
+
if (group === "plans")
|
|
1218
|
+
return await handlePlans(subcommand, rest, parsed.globals);
|
|
1219
|
+
if (group === "docs")
|
|
1220
|
+
return await handleDocs(subcommand, rest, parsed.globals);
|
|
1221
|
+
if (group === "queue" || group === "sync")
|
|
1222
|
+
return await handleQueue(group === "sync" ? "sync" : subcommand, rest, parsed.globals);
|
|
1223
|
+
if (group === "completion" || group === "completions")
|
|
1224
|
+
return handleCompletion(subcommand);
|
|
1225
|
+
if (group === "manpage" || group === "manual") {
|
|
1226
|
+
printOutput(renderManpage(), parsed.globals);
|
|
1227
|
+
return 0;
|
|
1228
|
+
}
|
|
1229
|
+
if (group === "config")
|
|
1230
|
+
return await handleConfig(subcommand, rest, parsed.globals);
|
|
1231
|
+
if (group === "api")
|
|
1232
|
+
return await handleApi(subcommand, rest, parsed.globals);
|
|
1233
|
+
throw new UsageError(`Unknown command: ${group}`);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
return handleError(error, parsed.globals);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function handleApiKeys(subcommand, args, globals) {
|
|
1239
|
+
const client = new PlatformTodosClient(globals);
|
|
1240
|
+
const options = parseOptions(args);
|
|
1241
|
+
if (subcommand === "list" || !subcommand) {
|
|
1242
|
+
printOutput(await client.listApiKeys(), globals);
|
|
1243
|
+
return 0;
|
|
1244
|
+
}
|
|
1245
|
+
if (subcommand === "create") {
|
|
1246
|
+
const created = await client.createManagedApiKey({
|
|
1247
|
+
name: options.name ?? firstPositional(args) ?? "platform-todos-cli",
|
|
1248
|
+
...options.scopes ? { scopes: splitList(options.scopes) } : {},
|
|
1249
|
+
...options.serviceAccount ? { serviceAccountId: options.serviceAccount } : {},
|
|
1250
|
+
...options.serviceAccountId ? { serviceAccountId: options.serviceAccountId } : {}
|
|
1251
|
+
});
|
|
1252
|
+
printOutput(selectApiKeyIfRequested(created, options, globals), globals);
|
|
1253
|
+
return 0;
|
|
1254
|
+
}
|
|
1255
|
+
if (subcommand === "select" || subcommand === "use") {
|
|
1256
|
+
const apiKey = options.key ?? options.apiKey ?? firstPositional(args) ?? globals.apiKey;
|
|
1257
|
+
if (!apiKey) {
|
|
1258
|
+
throw new UsageError("API key is required. Use: platform-todos api-keys select --key <api-key>");
|
|
1259
|
+
}
|
|
1260
|
+
printOutput(selectApiKey(apiKey, options, globals), globals);
|
|
1261
|
+
return 0;
|
|
1262
|
+
}
|
|
1263
|
+
if (subcommand === "revoke") {
|
|
1264
|
+
const id = options.id ?? firstPositional(args);
|
|
1265
|
+
if (!id)
|
|
1266
|
+
throw new UsageError("API key ID is required.");
|
|
1267
|
+
printOutput(await client.revokeApiKey(id), globals);
|
|
1268
|
+
return 0;
|
|
1269
|
+
}
|
|
1270
|
+
if (subcommand === "rotate") {
|
|
1271
|
+
const id = options.id ?? firstPositional(args);
|
|
1272
|
+
if (!id)
|
|
1273
|
+
throw new UsageError("API key ID is required.");
|
|
1274
|
+
const rotated = await client.rotateApiKey(id, options.name);
|
|
1275
|
+
printOutput(selectApiKeyIfRequested(rotated, options, globals), globals);
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
1278
|
+
throw new UsageError("Usage: platform-todos api-keys <list|create|select|revoke|rotate>");
|
|
1279
|
+
}
|
|
1280
|
+
async function handleServiceAccounts(subcommand, args, globals) {
|
|
1281
|
+
const client = new PlatformTodosClient(globals);
|
|
1282
|
+
const options = parseOptions(args);
|
|
1283
|
+
if (subcommand === "list" || !subcommand) {
|
|
1284
|
+
printOutput(await client.listServiceAccounts(), globals);
|
|
1285
|
+
return 0;
|
|
1286
|
+
}
|
|
1287
|
+
if (subcommand === "create") {
|
|
1288
|
+
const name = options.name ?? firstPositional(args);
|
|
1289
|
+
if (!name)
|
|
1290
|
+
throw new UsageError("Service account name is required.");
|
|
1291
|
+
printOutput(await client.createServiceAccount({
|
|
1292
|
+
name,
|
|
1293
|
+
...options.description ? { description: options.description } : {},
|
|
1294
|
+
...options.scopes ? { scopes: splitList(options.scopes) } : {}
|
|
1295
|
+
}), globals);
|
|
1296
|
+
return 0;
|
|
1297
|
+
}
|
|
1298
|
+
if (subcommand === "key") {
|
|
1299
|
+
const serviceAccountId = options.id ?? options.serviceAccountId ?? firstPositional(args);
|
|
1300
|
+
if (!serviceAccountId)
|
|
1301
|
+
throw new UsageError("Service account ID is required.");
|
|
1302
|
+
printOutput(await client.createManagedApiKey({
|
|
1303
|
+
serviceAccountId,
|
|
1304
|
+
name: options.name ?? "service-account-key",
|
|
1305
|
+
...options.scopes ? { scopes: splitList(options.scopes) } : {}
|
|
1306
|
+
}), globals);
|
|
1307
|
+
return 0;
|
|
1308
|
+
}
|
|
1309
|
+
throw new UsageError("Usage: platform-todos service-accounts <list|create|key>");
|
|
1310
|
+
}
|
|
1311
|
+
async function handleSandbox(subcommand, args, globals) {
|
|
1312
|
+
const client = new PlatformTodosClient(globals);
|
|
1313
|
+
const options = parseOptions(args);
|
|
1314
|
+
if (subcommand === "policies" || subcommand === "list" || !subcommand) {
|
|
1315
|
+
printOutput(await client.listSandboxPolicies(), globals);
|
|
1316
|
+
return 0;
|
|
1317
|
+
}
|
|
1318
|
+
if (subcommand === "set") {
|
|
1319
|
+
const metadata = options.metadata ? JSON.parse(options.metadata) : undefined;
|
|
1320
|
+
printOutput(await client.setSandboxPolicy({
|
|
1321
|
+
...options.agent ? { agentId: options.agent } : {},
|
|
1322
|
+
...options.allowTools ? { allowedTools: splitCsv(options.allowTools) } : {},
|
|
1323
|
+
...options.denyTools ? { deniedTools: splitCsv(options.denyTools) } : {},
|
|
1324
|
+
...options.network ? { network: options.network } : {},
|
|
1325
|
+
...options.filesystem ? { filesystem: options.filesystem } : {},
|
|
1326
|
+
...options.redactEnv ? { envRedactionPatterns: splitCsv(options.redactEnv) } : {},
|
|
1327
|
+
...metadata ? { metadata } : {}
|
|
1328
|
+
}), globals);
|
|
1329
|
+
return 0;
|
|
1330
|
+
}
|
|
1331
|
+
if (subcommand === "delete") {
|
|
1332
|
+
printOutput(await client.deleteSandboxPolicy({
|
|
1333
|
+
...options.agent ? { agentId: options.agent } : {}
|
|
1334
|
+
}), globals);
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
throw new UsageError("Usage: platform-todos sandbox <policies|set|delete>");
|
|
1338
|
+
}
|
|
1339
|
+
async function handleAuth(subcommand, args, globals) {
|
|
1340
|
+
if (subcommand === "login" || subcommand === "signup") {
|
|
1341
|
+
const options = parseOptions(args);
|
|
1342
|
+
const apiUrl = options.apiUrl ?? globals.apiUrl ?? DEFAULT_API_URL;
|
|
1343
|
+
const apiKey = options.apiKey ?? globals.apiKey;
|
|
1344
|
+
const authFile = authFileForProfile(globals.profile);
|
|
1345
|
+
if (apiKey) {
|
|
1346
|
+
writeAuthConfig({ apiUrl, apiKey }, authFile);
|
|
1347
|
+
printOutput({ status: "authenticated", apiUrl, apiKey: redactSecret(apiKey), source: "api-key" }, globals);
|
|
1348
|
+
return 0;
|
|
1349
|
+
}
|
|
1350
|
+
let email = options.email;
|
|
1351
|
+
if (!email && !globals.nonInteractive && isTTY()) {
|
|
1352
|
+
email = await prompt("Email: ");
|
|
1353
|
+
}
|
|
1354
|
+
if (!email || !email.includes("@")) {
|
|
1355
|
+
throw new UsageError("Email required. Use: platform-todos auth login --email you@example.com");
|
|
1356
|
+
}
|
|
1357
|
+
const client = new PlatformTodosClient({ apiUrl });
|
|
1358
|
+
let code = options.code;
|
|
1359
|
+
if (!code) {
|
|
1360
|
+
const login = await client.login(email);
|
|
1361
|
+
if (globals.nonInteractive || !isTTY()) {
|
|
1362
|
+
printOutput({
|
|
1363
|
+
status: "code_required",
|
|
1364
|
+
email: normalizeEmail(email),
|
|
1365
|
+
message: `${login.message ?? "Enter the verification code."} Run: platform-todos auth login --email ${normalizeEmail(email)} --code <code>`
|
|
1366
|
+
}, globals);
|
|
1367
|
+
return 0;
|
|
1368
|
+
}
|
|
1369
|
+
code = await promptSecret("Code: ");
|
|
1370
|
+
}
|
|
1371
|
+
if (!code)
|
|
1372
|
+
throw new UsageError("Verification code is required.");
|
|
1373
|
+
const verified = await client.verify(email, code);
|
|
1374
|
+
const key = verified.apiKey ?? await createApiKeyFromSession(client, verified.token, {
|
|
1375
|
+
organizationId: verified.organization?.id,
|
|
1376
|
+
email: verified.user?.email ?? normalizeEmail(email)
|
|
1377
|
+
});
|
|
1378
|
+
writeAuthConfig({
|
|
1379
|
+
apiUrl,
|
|
1380
|
+
apiKey: key,
|
|
1381
|
+
email: verified.user?.email ?? normalizeEmail(email),
|
|
1382
|
+
organizationId: verified.organization?.id,
|
|
1383
|
+
organizationSlug: verified.organization?.slug,
|
|
1384
|
+
userId: verified.user?.id
|
|
1385
|
+
}, authFile);
|
|
1386
|
+
printOutput({
|
|
1387
|
+
status: "authenticated",
|
|
1388
|
+
email: verified.user?.email ?? normalizeEmail(email),
|
|
1389
|
+
organization: verified.organization?.slug ?? null,
|
|
1390
|
+
apiUrl
|
|
1391
|
+
}, globals);
|
|
1392
|
+
return 0;
|
|
1393
|
+
}
|
|
1394
|
+
if (subcommand === "logout") {
|
|
1395
|
+
clearAuthConfig(authFileForProfile(globals.profile));
|
|
1396
|
+
printOutput({ status: "signed_out" }, globals);
|
|
1397
|
+
return 0;
|
|
1398
|
+
}
|
|
1399
|
+
if (subcommand === "whoami") {
|
|
1400
|
+
printOutput(await new PlatformTodosClient(globals).whoami(), globals);
|
|
1401
|
+
return 0;
|
|
1402
|
+
}
|
|
1403
|
+
if (subcommand === "status") {
|
|
1404
|
+
const auth = resolveAuthContext(globals);
|
|
1405
|
+
printOutput({
|
|
1406
|
+
signedIn: Boolean(auth.apiKey),
|
|
1407
|
+
profile: normalizeProfile(globals.profile ?? process.env.PLATFORM_TODOS_PROFILE ?? process.env.TODOS_PROFILE) ?? "default",
|
|
1408
|
+
apiUrl: auth.apiUrl,
|
|
1409
|
+
apiKey: redactSecret(auth.apiKey),
|
|
1410
|
+
source: auth.source,
|
|
1411
|
+
email: auth.stored?.email ?? null,
|
|
1412
|
+
organization: auth.stored?.organizationSlug ?? null
|
|
1413
|
+
}, globals);
|
|
1414
|
+
return 0;
|
|
1415
|
+
}
|
|
1416
|
+
throw new UsageError("Usage: platform-todos auth <login|signup|logout|whoami|status>");
|
|
1417
|
+
}
|
|
1418
|
+
async function handleBilling(subcommand, args, globals) {
|
|
1419
|
+
const client = new PlatformTodosClient(globals);
|
|
1420
|
+
const options = parseOptions(args);
|
|
1421
|
+
if (subcommand === "status" || !subcommand) {
|
|
1422
|
+
printOutput(await client.billingStatus(), globals);
|
|
1423
|
+
return 0;
|
|
1424
|
+
}
|
|
1425
|
+
if (subcommand === "usage") {
|
|
1426
|
+
printOutput(await client.billingUsage(), globals);
|
|
1427
|
+
return 0;
|
|
1428
|
+
}
|
|
1429
|
+
if (subcommand === "checkout") {
|
|
1430
|
+
printOutput(openBillingSessionIfRequested(await client.billingCheckout(options.plan), options), globals);
|
|
1431
|
+
return 0;
|
|
1432
|
+
}
|
|
1433
|
+
if (subcommand === "portal") {
|
|
1434
|
+
printOutput(openBillingSessionIfRequested(await client.billingPortal(), options), globals);
|
|
1435
|
+
return 0;
|
|
1436
|
+
}
|
|
1437
|
+
throw new UsageError("Usage: platform-todos billing <status|usage|checkout|portal> [--plan pro|team|storage] [--open]");
|
|
1438
|
+
}
|
|
1439
|
+
async function handleOrganization(subcommand, args, globals) {
|
|
1440
|
+
const client = new PlatformTodosClient(globals);
|
|
1441
|
+
const options = parseOptions(args);
|
|
1442
|
+
if (subcommand === "show" || subcommand === "status" || !subcommand) {
|
|
1443
|
+
printOutput(await client.organizationSummary(), globals);
|
|
1444
|
+
return 0;
|
|
1445
|
+
}
|
|
1446
|
+
if (subcommand === "members") {
|
|
1447
|
+
const summary = await client.organizationSummary();
|
|
1448
|
+
printOutput(summary.members ?? [], globals);
|
|
1449
|
+
return 0;
|
|
1450
|
+
}
|
|
1451
|
+
if (subcommand === "invitations") {
|
|
1452
|
+
const summary = await client.organizationSummary();
|
|
1453
|
+
printOutput(summary.invitations ?? [], globals);
|
|
1454
|
+
return 0;
|
|
1455
|
+
}
|
|
1456
|
+
if (subcommand === "audit") {
|
|
1457
|
+
const summary = await client.organizationSummary();
|
|
1458
|
+
printOutput(summary.auditEvents ?? [], globals);
|
|
1459
|
+
return 0;
|
|
1460
|
+
}
|
|
1461
|
+
if (subcommand === "update") {
|
|
1462
|
+
const settings = options.settings ? JSON.parse(options.settings) : undefined;
|
|
1463
|
+
printOutput(await client.updateOrganization({
|
|
1464
|
+
...options.name ? { name: options.name } : {},
|
|
1465
|
+
...settings ? { settings } : {}
|
|
1466
|
+
}), globals);
|
|
1467
|
+
return 0;
|
|
1468
|
+
}
|
|
1469
|
+
if (subcommand === "invite") {
|
|
1470
|
+
const email = options.email ?? firstPositional(args);
|
|
1471
|
+
if (!email)
|
|
1472
|
+
throw new UsageError("Invite email is required.");
|
|
1473
|
+
printOutput(await client.createOrganizationInvitation({
|
|
1474
|
+
email,
|
|
1475
|
+
role: options.role ?? "member",
|
|
1476
|
+
...options.expiresInDays ? { expiresInDays: Number(options.expiresInDays) } : {}
|
|
1477
|
+
}), globals);
|
|
1478
|
+
return 0;
|
|
1479
|
+
}
|
|
1480
|
+
if (subcommand === "accept-invite") {
|
|
1481
|
+
const token = options.token ?? firstPositional(args);
|
|
1482
|
+
if (!token)
|
|
1483
|
+
throw new UsageError("Invitation token is required.");
|
|
1484
|
+
printOutput(await client.acceptOrganizationInvitation({
|
|
1485
|
+
token,
|
|
1486
|
+
...options.invitationId ? { invitationId: options.invitationId } : {}
|
|
1487
|
+
}), globals);
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
if (subcommand === "revoke-invite") {
|
|
1491
|
+
const id = options.id ?? firstPositional(args);
|
|
1492
|
+
if (!id)
|
|
1493
|
+
throw new UsageError("Invitation ID is required.");
|
|
1494
|
+
printOutput(await client.revokeOrganizationInvitation(id), globals);
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1497
|
+
if (subcommand === "role") {
|
|
1498
|
+
const userId = options.userId ?? firstPositional(args);
|
|
1499
|
+
const role = options.role ?? secondPositional(args);
|
|
1500
|
+
if (!userId)
|
|
1501
|
+
throw new UsageError("Member user ID is required.");
|
|
1502
|
+
if (!role)
|
|
1503
|
+
throw new UsageError("Role is required.");
|
|
1504
|
+
printOutput(await client.changeOrganizationMemberRole(userId, role), globals);
|
|
1505
|
+
return 0;
|
|
1506
|
+
}
|
|
1507
|
+
if (subcommand === "remove") {
|
|
1508
|
+
const userId = options.userId ?? firstPositional(args);
|
|
1509
|
+
if (!userId)
|
|
1510
|
+
throw new UsageError("Member user ID is required.");
|
|
1511
|
+
printOutput(await client.removeOrganizationMember(userId), globals);
|
|
1512
|
+
return 0;
|
|
1513
|
+
}
|
|
1514
|
+
throw new UsageError("Usage: platform-todos organization <show|members|invitations|audit|update|invite|accept-invite|revoke-invite|role|remove>");
|
|
1515
|
+
}
|
|
1516
|
+
async function handleAudit(subcommand, args, globals) {
|
|
1517
|
+
if (subcommand !== "export" && subcommand !== "evidence") {
|
|
1518
|
+
throw new UsageError("Usage: platform-todos audit export --body <json>|--file <path> [--idempotency-key <key>]");
|
|
1519
|
+
}
|
|
1520
|
+
const options = parseOptions(args);
|
|
1521
|
+
const rawBody = options.body ?? (options.file ? readFileSync3(options.file, "utf8") : undefined);
|
|
1522
|
+
if (!rawBody)
|
|
1523
|
+
throw new UsageError("Audit export body is required. Use --body <json> or --file <path>.");
|
|
1524
|
+
printOutput(await new PlatformTodosClient(globals).createAuditEvidenceExport(JSON.parse(rawBody), options.idempotencyKey ?? options.idempotency), globals);
|
|
1525
|
+
return 0;
|
|
1526
|
+
}
|
|
1527
|
+
async function handlePrivacy(subcommand, args, globals) {
|
|
1528
|
+
if (subcommand !== "export" && subcommand !== "delete" && subcommand !== "deletion" && subcommand !== "retention") {
|
|
1529
|
+
throw new UsageError("Usage: platform-todos privacy <export|delete|retention> --body <json>|--file <path> [--idempotency-key <key>]");
|
|
1530
|
+
}
|
|
1531
|
+
const options = parseOptions(args);
|
|
1532
|
+
const rawBody = options.body ?? (options.file ? readFileSync3(options.file, "utf8") : "{}");
|
|
1533
|
+
const body = JSON.parse(rawBody);
|
|
1534
|
+
const idempotencyKey = options.idempotencyKey ?? options.idempotency;
|
|
1535
|
+
const client = new PlatformTodosClient(globals);
|
|
1536
|
+
if (subcommand === "export") {
|
|
1537
|
+
printOutput(await client.createPrivacyExport(body, idempotencyKey), globals);
|
|
1538
|
+
return 0;
|
|
1539
|
+
}
|
|
1540
|
+
if (subcommand === "retention") {
|
|
1541
|
+
printOutput(await client.enforcePrivacyRetention(body, idempotencyKey), globals);
|
|
1542
|
+
return 0;
|
|
1543
|
+
}
|
|
1544
|
+
printOutput(await client.executePrivacyDeletion(body, idempotencyKey), globals);
|
|
1545
|
+
return 0;
|
|
1546
|
+
}
|
|
1547
|
+
async function handleSearch(subcommand, args, globals) {
|
|
1548
|
+
const searchArgs = subcommand && subcommand !== "query" && subcommand !== "workspace" ? [subcommand, ...args] : args;
|
|
1549
|
+
const options = parseOptions(searchArgs);
|
|
1550
|
+
const positionalQuery = firstPositional(searchArgs);
|
|
1551
|
+
const rawBody = options.body ?? (options.file ? readFileSync3(options.file, "utf8") : "{}");
|
|
1552
|
+
const body = JSON.parse(rawBody);
|
|
1553
|
+
const query = options.query ?? options.q ?? positionalQuery;
|
|
1554
|
+
printOutput(await new PlatformTodosClient(globals).search({
|
|
1555
|
+
...body,
|
|
1556
|
+
...query ? { query } : {},
|
|
1557
|
+
...options.type ? { types: options.type.split(",").map((type) => type.trim()).filter(Boolean) } : {},
|
|
1558
|
+
...options.types ? { types: options.types.split(",").map((type) => type.trim()).filter(Boolean) } : {},
|
|
1559
|
+
...options.status ? { status: options.status } : {},
|
|
1560
|
+
...options.limit ? { limit: readNumericOption(options.limit) } : {},
|
|
1561
|
+
...options.cursor ? { cursor: options.cursor } : {}
|
|
1562
|
+
}), globals);
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
async function handleRuns(subcommand, args, globals) {
|
|
1566
|
+
const client = new PlatformTodosClient(globals);
|
|
1567
|
+
const options = parseOptions(args);
|
|
1568
|
+
const idempotencyKey = options.idempotencyKey ?? options.idempotency;
|
|
1569
|
+
if (subcommand === "list" || !subcommand) {
|
|
1570
|
+
printOutput(await client.listRuns(), globals);
|
|
1571
|
+
return 0;
|
|
1572
|
+
}
|
|
1573
|
+
if (subcommand === "create" || subcommand === "run") {
|
|
1574
|
+
const slug = firstPositional(args);
|
|
1575
|
+
if (!slug)
|
|
1576
|
+
throw new UsageError("Run slug is required.");
|
|
1577
|
+
const body = options.body ? JSON.parse(options.body) : {};
|
|
1578
|
+
printOutput(await client.createRun(slug, {
|
|
1579
|
+
...body,
|
|
1580
|
+
...options.maxAttempts ? { maxAttempts: Number(options.maxAttempts) } : {},
|
|
1581
|
+
...options.timeoutMs ? { timeoutMs: Number(options.timeoutMs) } : {}
|
|
1582
|
+
}), globals);
|
|
1583
|
+
return 0;
|
|
1584
|
+
}
|
|
1585
|
+
if (subcommand === "show") {
|
|
1586
|
+
const id = firstPositional(args);
|
|
1587
|
+
if (!id)
|
|
1588
|
+
throw new UsageError("Run ID is required.");
|
|
1589
|
+
printOutput(await client.getRun(id), globals);
|
|
1590
|
+
return 0;
|
|
1591
|
+
}
|
|
1592
|
+
if (subcommand === "cancel") {
|
|
1593
|
+
const id = firstPositional(args);
|
|
1594
|
+
if (!id)
|
|
1595
|
+
throw new UsageError("Run ID is required.");
|
|
1596
|
+
printOutput(await client.cancelRun(id), globals);
|
|
1597
|
+
return 0;
|
|
1598
|
+
}
|
|
1599
|
+
if (subcommand === "logs") {
|
|
1600
|
+
const id = firstPositional(args);
|
|
1601
|
+
if (!id)
|
|
1602
|
+
throw new UsageError("Run ID is required.");
|
|
1603
|
+
printOutput(await client.listRunLogs(id), globals);
|
|
1604
|
+
return 0;
|
|
1605
|
+
}
|
|
1606
|
+
if (subcommand === "artifacts") {
|
|
1607
|
+
const id = firstPositional(args);
|
|
1608
|
+
if (!id)
|
|
1609
|
+
throw new UsageError("Run ID is required.");
|
|
1610
|
+
printOutput(await client.listRunArtifacts(id), globals);
|
|
1611
|
+
return 0;
|
|
1612
|
+
}
|
|
1613
|
+
if (subcommand === "controls" || subcommand === "budgets") {
|
|
1614
|
+
if (!args.some((arg) => arg.startsWith("--"))) {
|
|
1615
|
+
printOutput(await client.getRunControls(), globals);
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
printOutput(await client.updateRunControls(readRunControlOptions(options), idempotencyKey), globals);
|
|
1619
|
+
return 0;
|
|
1620
|
+
}
|
|
1621
|
+
if (subcommand === "pause") {
|
|
1622
|
+
printOutput(await client.pauseRuns({ reason: options.reason }, idempotencyKey), globals);
|
|
1623
|
+
return 0;
|
|
1624
|
+
}
|
|
1625
|
+
if (subcommand === "resume") {
|
|
1626
|
+
printOutput(await client.resumeRuns(idempotencyKey), globals);
|
|
1627
|
+
return 0;
|
|
1628
|
+
}
|
|
1629
|
+
if (subcommand === "emergency-stop" || subcommand === "stop") {
|
|
1630
|
+
printOutput(await client.emergencyStopRuns({ reason: options.reason }, idempotencyKey), globals);
|
|
1631
|
+
return 0;
|
|
1632
|
+
}
|
|
1633
|
+
if (subcommand === "usage") {
|
|
1634
|
+
const runId = firstPositional(args);
|
|
1635
|
+
if (!runId)
|
|
1636
|
+
throw new UsageError("Run ID is required.");
|
|
1637
|
+
printOutput(await client.recordRunUsage(runId, {
|
|
1638
|
+
costCents: readNumericOption(options.costCents),
|
|
1639
|
+
toolCalls: readNumericOption(options.toolCalls),
|
|
1640
|
+
durationMs: readNumericOption(options.durationMs)
|
|
1641
|
+
}, idempotencyKey), globals);
|
|
1642
|
+
return 0;
|
|
1643
|
+
}
|
|
1644
|
+
throw new UsageError("Usage: platform-todos runs <list|create|show|cancel|logs|artifacts|controls|pause|resume|emergency-stop|usage>");
|
|
1645
|
+
}
|
|
1646
|
+
async function handleTasks(subcommand, args, globals) {
|
|
1647
|
+
const client = new PlatformTodosClient(globals);
|
|
1648
|
+
const offline = hasBooleanOption(args, "offline");
|
|
1649
|
+
const queueOffline = hasBooleanOption(args, "queue-offline");
|
|
1650
|
+
const dryRun = hasBooleanOption(args, "dry-run");
|
|
1651
|
+
const cleanedArgs = withoutBooleanOptions(args, ["offline", "queue-offline", "dry-run"]);
|
|
1652
|
+
const options = parseOptions(cleanedArgs);
|
|
1653
|
+
if (subcommand === "list" || !subcommand) {
|
|
1654
|
+
const query = buildQuery({
|
|
1655
|
+
status: options.status,
|
|
1656
|
+
project_id: options.project,
|
|
1657
|
+
plan_id: options.plan,
|
|
1658
|
+
assigned_to: options.assigned,
|
|
1659
|
+
overdue: options.overdue,
|
|
1660
|
+
ready: options.ready,
|
|
1661
|
+
due_before: options.dueBefore,
|
|
1662
|
+
limit: options.limit
|
|
1663
|
+
});
|
|
1664
|
+
printOutput(await client.listTasks(query), globals);
|
|
1665
|
+
return 0;
|
|
1666
|
+
}
|
|
1667
|
+
if (subcommand === "show") {
|
|
1668
|
+
const id = firstPositional(args);
|
|
1669
|
+
if (!id)
|
|
1670
|
+
throw new UsageError("Task ID is required.");
|
|
1671
|
+
printOutput(await client.getTask(id), globals);
|
|
1672
|
+
return 0;
|
|
1673
|
+
}
|
|
1674
|
+
if (subcommand === "add") {
|
|
1675
|
+
const title = firstPositional(cleanedArgs);
|
|
1676
|
+
if (!title)
|
|
1677
|
+
throw new UsageError("Task title is required.");
|
|
1678
|
+
const input2 = {
|
|
1679
|
+
title,
|
|
1680
|
+
description: options.description,
|
|
1681
|
+
priority: options.priority,
|
|
1682
|
+
project_id: options.project,
|
|
1683
|
+
plan_id: options.plan,
|
|
1684
|
+
assigned_to: options.assigned,
|
|
1685
|
+
due_at: options.due,
|
|
1686
|
+
start_after: options.startAfter,
|
|
1687
|
+
recurrence_rule: options.recurrence,
|
|
1688
|
+
sla_minutes: options.slaMinutes ? Number(options.slaMinutes) : undefined,
|
|
1689
|
+
tags: options.tags ? options.tags.split(",").map((tag) => tag.trim()).filter(Boolean) : undefined
|
|
1690
|
+
};
|
|
1691
|
+
if (offline) {
|
|
1692
|
+
printOutput(queuedOutput(enqueueOfflineRequest({
|
|
1693
|
+
operation: "tasks.add",
|
|
1694
|
+
path: "/api/tasks",
|
|
1695
|
+
body: input2,
|
|
1696
|
+
profile: globals.profile,
|
|
1697
|
+
idempotencyKey: options.idempotencyKey ?? options.idempotency
|
|
1698
|
+
})), globals);
|
|
1699
|
+
return 0;
|
|
1700
|
+
}
|
|
1701
|
+
printOutput(await requestOrQueue({
|
|
1702
|
+
client,
|
|
1703
|
+
globals,
|
|
1704
|
+
queueOffline,
|
|
1705
|
+
operation: "tasks.add",
|
|
1706
|
+
path: "/api/tasks",
|
|
1707
|
+
body: input2,
|
|
1708
|
+
idempotencyKey: options.idempotencyKey ?? options.idempotency,
|
|
1709
|
+
execute: () => client.request("/api/tasks", {
|
|
1710
|
+
method: "POST",
|
|
1711
|
+
body: input2,
|
|
1712
|
+
headers: idempotencyHeaders(options)
|
|
1713
|
+
})
|
|
1714
|
+
}), globals);
|
|
1715
|
+
return 0;
|
|
1716
|
+
}
|
|
1717
|
+
if (subcommand === "claim") {
|
|
1718
|
+
printOutput(await client.claimTask({
|
|
1719
|
+
worker: options.worker
|
|
1720
|
+
}), globals);
|
|
1721
|
+
return 0;
|
|
1722
|
+
}
|
|
1723
|
+
if (subcommand === "start" || subcommand === "done") {
|
|
1724
|
+
const id = firstPositional(cleanedArgs);
|
|
1725
|
+
if (!id)
|
|
1726
|
+
throw new UsageError("Task ID is required.");
|
|
1727
|
+
const operation = subcommand === "start" ? "tasks.start" : "tasks.done";
|
|
1728
|
+
const path = `/api/tasks/${encodeURIComponent(id)}/${subcommand === "start" ? "start" : "complete"}`;
|
|
1729
|
+
if (offline) {
|
|
1730
|
+
printOutput(queuedOutput(enqueueOfflineRequest({
|
|
1731
|
+
operation,
|
|
1732
|
+
path,
|
|
1733
|
+
profile: globals.profile,
|
|
1734
|
+
idempotencyKey: options.idempotencyKey ?? options.idempotency
|
|
1735
|
+
})), globals);
|
|
1736
|
+
return 0;
|
|
1737
|
+
}
|
|
1738
|
+
printOutput(await requestOrQueue({
|
|
1739
|
+
client,
|
|
1740
|
+
globals,
|
|
1741
|
+
queueOffline,
|
|
1742
|
+
operation,
|
|
1743
|
+
path,
|
|
1744
|
+
idempotencyKey: options.idempotencyKey ?? options.idempotency,
|
|
1745
|
+
execute: () => client.request(path, {
|
|
1746
|
+
method: "POST",
|
|
1747
|
+
headers: idempotencyHeaders(options)
|
|
1748
|
+
})
|
|
1749
|
+
}), globals);
|
|
1750
|
+
return 0;
|
|
1751
|
+
}
|
|
1752
|
+
if (subcommand === "dependencies" || subcommand === "deps") {
|
|
1753
|
+
printOutput(await client.listTaskDependencies(options.task ?? options.taskId), globals);
|
|
1754
|
+
return 0;
|
|
1755
|
+
}
|
|
1756
|
+
if (subcommand === "depends") {
|
|
1757
|
+
const taskId = options.taskId ?? firstPositional(args);
|
|
1758
|
+
const dependsOnTaskId = options.dependsOnTaskId ?? secondPositional(args);
|
|
1759
|
+
if (!taskId || !dependsOnTaskId)
|
|
1760
|
+
throw new UsageError("Usage: platform-todos tasks depends <task-id> <depends-on-task-id> [--reason <text>]");
|
|
1761
|
+
printOutput(await client.createTaskDependency({
|
|
1762
|
+
taskId,
|
|
1763
|
+
dependsOnTaskId,
|
|
1764
|
+
reason: options.reason
|
|
1765
|
+
}), globals);
|
|
1766
|
+
return 0;
|
|
1767
|
+
}
|
|
1768
|
+
if (subcommand === "unblock" || subcommand === "dependency-clear") {
|
|
1769
|
+
const id = options.id ?? firstPositional(args);
|
|
1770
|
+
if (!id)
|
|
1771
|
+
throw new UsageError("Dependency ID is required.");
|
|
1772
|
+
printOutput(await client.clearTaskDependency(id), globals);
|
|
1773
|
+
return 0;
|
|
1774
|
+
}
|
|
1775
|
+
if (subcommand === "ready" || subcommand === "next") {
|
|
1776
|
+
const taskIds = readCsvOption(options.taskIds ?? options.tasks);
|
|
1777
|
+
if (taskIds.length === 0)
|
|
1778
|
+
throw new UsageError("--task-ids is required.");
|
|
1779
|
+
const input2 = {
|
|
1780
|
+
taskIds,
|
|
1781
|
+
completedTaskIds: readCsvOption(options.completedTaskIds ?? options.completed),
|
|
1782
|
+
claimedTaskIds: readCsvOption(options.claimedTaskIds ?? options.claimed)
|
|
1783
|
+
};
|
|
1784
|
+
printOutput(subcommand === "ready" ? await client.readyTasks(input2) : await client.nextReadyTask(input2), globals);
|
|
1785
|
+
return 0;
|
|
1786
|
+
}
|
|
1787
|
+
if (subcommand === "batch") {
|
|
1788
|
+
const rawBody = options.body ?? (options.file ? readFileSync3(options.file, "utf8") : undefined);
|
|
1789
|
+
if (!rawBody)
|
|
1790
|
+
throw new UsageError("Batch body is required. Use --body <json> or --file <path>.");
|
|
1791
|
+
const body = JSON.parse(rawBody);
|
|
1792
|
+
if (!Array.isArray(body.operations))
|
|
1793
|
+
throw new UsageError("Batch body must include an operations array.");
|
|
1794
|
+
if (dryRun)
|
|
1795
|
+
body.dryRun = true;
|
|
1796
|
+
printOutput(await client.batchTasks(body, options.idempotencyKey ?? options.idempotency), globals);
|
|
1797
|
+
return 0;
|
|
1798
|
+
}
|
|
1799
|
+
throw new UsageError("Usage: platform-todos tasks <list|show|add|claim|start|done|batch|dependencies|depends|unblock|ready|next> [--offline|--queue-offline]");
|
|
1800
|
+
}
|
|
1801
|
+
function readRunControlOptions(options) {
|
|
1802
|
+
return {
|
|
1803
|
+
budgetCents: readNumericOption(options.budgetCents),
|
|
1804
|
+
maxRunCostCents: readNumericOption(options.maxRunCostCents),
|
|
1805
|
+
maxRunDurationMs: readNumericOption(options.maxRunDurationMs),
|
|
1806
|
+
maxRunToolCalls: readNumericOption(options.maxRunToolCalls),
|
|
1807
|
+
...options.paused ? { paused: options.paused === "true" } : {},
|
|
1808
|
+
...options.reason ? { pauseReason: options.reason } : {},
|
|
1809
|
+
...options.agentId ? { agentId: options.agentId } : {},
|
|
1810
|
+
...options.allowedTools ? { allowedTools: readCsvOption(options.allowedTools) } : {},
|
|
1811
|
+
...options.allowTools ? { allowedTools: readCsvOption(options.allowTools) } : {},
|
|
1812
|
+
...options.deniedTools ? { deniedTools: readCsvOption(options.deniedTools) } : {},
|
|
1813
|
+
...options.denyTools ? { deniedTools: readCsvOption(options.denyTools) } : {},
|
|
1814
|
+
...options.networkPolicy ? { networkPolicy: options.networkPolicy } : {},
|
|
1815
|
+
...options.allowedHosts ? { allowedHosts: readCsvOption(options.allowedHosts) } : {},
|
|
1816
|
+
...options.allowHosts ? { allowedHosts: readCsvOption(options.allowHosts) } : {},
|
|
1817
|
+
...options.filesystemPolicy ? { filesystemPolicy: options.filesystemPolicy } : {},
|
|
1818
|
+
...options.writablePaths ? { writablePaths: readCsvOption(options.writablePaths) } : {},
|
|
1819
|
+
...options.redactEnv ? { redactEnv: readCsvOption(options.redactEnv) } : {}
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
function readCsvOption(value) {
|
|
1823
|
+
return value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
|
|
1824
|
+
}
|
|
1825
|
+
function readNumericOption(value) {
|
|
1826
|
+
if (!value)
|
|
1827
|
+
return;
|
|
1828
|
+
const parsed = Number(value);
|
|
1829
|
+
if (!Number.isFinite(parsed))
|
|
1830
|
+
throw new UsageError(`Expected numeric value, received: ${value}`);
|
|
1831
|
+
return Math.max(0, Math.floor(parsed));
|
|
1832
|
+
}
|
|
1833
|
+
async function handleQueue(subcommand, _args, globals) {
|
|
1834
|
+
const queuePath = offlineQueueFileForProfile(globals.profile);
|
|
1835
|
+
if (!subcommand || subcommand === "list" || subcommand === "status") {
|
|
1836
|
+
const items = readOfflineQueue(queuePath);
|
|
1837
|
+
printOutput({
|
|
1838
|
+
status: "ok",
|
|
1839
|
+
profile: normalizeProfile(globals.profile ?? process.env.PLATFORM_TODOS_PROFILE ?? process.env.TODOS_PROFILE) ?? "default",
|
|
1840
|
+
pending: items.filter((item) => item.status === "pending").length,
|
|
1841
|
+
conflicts: items.filter((item) => item.status === "conflict").length,
|
|
1842
|
+
items: items.map(queueItemSummary)
|
|
1843
|
+
}, globals);
|
|
1844
|
+
return 0;
|
|
1845
|
+
}
|
|
1846
|
+
if (subcommand === "sync") {
|
|
1847
|
+
const result = await syncOfflineQueue({
|
|
1848
|
+
client: new PlatformTodosClient(globals),
|
|
1849
|
+
profile: globals.profile
|
|
1850
|
+
});
|
|
1851
|
+
printOutput({
|
|
1852
|
+
status: result.retained.length === 0 ? "synced" : "partial",
|
|
1853
|
+
synced: result.synced.length,
|
|
1854
|
+
retained: result.retained.length,
|
|
1855
|
+
conflicts: result.retained.filter((item) => item.status === "conflict").length,
|
|
1856
|
+
items: result.retained.map(queueItemSummary)
|
|
1857
|
+
}, globals);
|
|
1858
|
+
return result.retained.some((item) => item.status === "conflict") ? 1 : 0;
|
|
1859
|
+
}
|
|
1860
|
+
if (subcommand === "clear") {
|
|
1861
|
+
clearOfflineQueue(queuePath);
|
|
1862
|
+
printOutput({ status: "cleared" }, globals);
|
|
1863
|
+
return 0;
|
|
1864
|
+
}
|
|
1865
|
+
throw new UsageError("Usage: platform-todos queue <list|sync|clear>");
|
|
1866
|
+
}
|
|
1867
|
+
async function handleNotifications(subcommand, args, globals) {
|
|
1868
|
+
const client = new PlatformTodosClient(globals);
|
|
1869
|
+
const options = parseOptions(args);
|
|
1870
|
+
if (subcommand === "list" || subcommand === "subscriptions" || !subcommand) {
|
|
1871
|
+
printOutput(await client.listNotificationSubscriptions(), globals);
|
|
1872
|
+
return 0;
|
|
1873
|
+
}
|
|
1874
|
+
if (subcommand === "create") {
|
|
1875
|
+
const targetUrl = options.url ?? options.targetUrl ?? firstPositional(args);
|
|
1876
|
+
if (!targetUrl)
|
|
1877
|
+
throw new UsageError("Webhook target URL is required.");
|
|
1878
|
+
const events = (options.events ?? "task.assigned,task.blocked,task.done,task.failed,run.completed").split(",").map((event) => event.trim()).filter(Boolean);
|
|
1879
|
+
printOutput(await client.createNotificationSubscription({
|
|
1880
|
+
targetUrl,
|
|
1881
|
+
events,
|
|
1882
|
+
...options.secret ? { secret: options.secret } : {}
|
|
1883
|
+
}), globals);
|
|
1884
|
+
return 0;
|
|
1885
|
+
}
|
|
1886
|
+
if (subcommand === "disable" || subcommand === "delete") {
|
|
1887
|
+
const id = options.id ?? firstPositional(args);
|
|
1888
|
+
if (!id)
|
|
1889
|
+
throw new UsageError("Notification subscription ID is required.");
|
|
1890
|
+
printOutput(await client.disableNotificationSubscription(id), globals);
|
|
1891
|
+
return 0;
|
|
1892
|
+
}
|
|
1893
|
+
if (subcommand === "deliveries" || subcommand === "dead-letters") {
|
|
1894
|
+
printOutput(await client.listNotificationDeliveries(), globals);
|
|
1895
|
+
return 0;
|
|
1896
|
+
}
|
|
1897
|
+
throw new UsageError("Usage: platform-todos notifications <list|create|disable|deliveries>");
|
|
1898
|
+
}
|
|
1899
|
+
async function handleApprovals(subcommand, args, globals) {
|
|
1900
|
+
const client = new PlatformTodosClient(globals);
|
|
1901
|
+
const options = parseOptions(args);
|
|
1902
|
+
if (subcommand === "list" || !subcommand) {
|
|
1903
|
+
const query = buildQuery({
|
|
1904
|
+
status: options.status,
|
|
1905
|
+
runId: options.runId ?? options.run
|
|
1906
|
+
});
|
|
1907
|
+
printOutput(await client.listApprovals(query), globals);
|
|
1908
|
+
return 0;
|
|
1909
|
+
}
|
|
1910
|
+
if (subcommand === "request") {
|
|
1911
|
+
const reason = options.reason ?? firstPositional(args);
|
|
1912
|
+
if (!reason)
|
|
1913
|
+
throw new UsageError("Approval reason is required.");
|
|
1914
|
+
printOutput(await client.requestApproval({
|
|
1915
|
+
reason,
|
|
1916
|
+
runId: options.runId ?? options.run,
|
|
1917
|
+
reviewerId: options.reviewerId ?? options.reviewer,
|
|
1918
|
+
checkpoint: options.checkpoint,
|
|
1919
|
+
escalationTarget: options.escalationTarget ?? options.escalateTo,
|
|
1920
|
+
expiresInSeconds: options.expiresInSeconds ?? options.timeoutSeconds ? Number(options.expiresInSeconds ?? options.timeoutSeconds) : undefined,
|
|
1921
|
+
metadata: options.metadata ? JSON.parse(options.metadata) : undefined
|
|
1922
|
+
}), globals);
|
|
1923
|
+
return 0;
|
|
1924
|
+
}
|
|
1925
|
+
if (subcommand === "approve" || subcommand === "reject") {
|
|
1926
|
+
const id = options.id ?? firstPositional(args);
|
|
1927
|
+
if (!id)
|
|
1928
|
+
throw new UsageError("Approval ID is required.");
|
|
1929
|
+
const reason = options.reason ?? secondPositional(args);
|
|
1930
|
+
printOutput(subcommand === "approve" ? await client.approveApproval(id, reason) : await client.rejectApproval(id, reason), globals);
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
if (subcommand === "expire") {
|
|
1934
|
+
const id = options.id ?? firstPositional(args);
|
|
1935
|
+
if (!id)
|
|
1936
|
+
throw new UsageError("Approval ID is required.");
|
|
1937
|
+
printOutput(await client.expireApproval(id), globals);
|
|
1938
|
+
return 0;
|
|
1939
|
+
}
|
|
1940
|
+
throw new UsageError("Usage: platform-todos approvals <list|request|approve|reject|expire>");
|
|
1941
|
+
}
|
|
1942
|
+
async function handleActivity(subcommand, args, globals) {
|
|
1943
|
+
const client = new PlatformTodosClient(globals);
|
|
1944
|
+
const options = parseOptions(args);
|
|
1945
|
+
if (subcommand === "list" || !subcommand) {
|
|
1946
|
+
printOutput(await client.listActivity(activityQuery(options)), globals);
|
|
1947
|
+
return 0;
|
|
1948
|
+
}
|
|
1949
|
+
if (subcommand === "event" || subcommand === "record") {
|
|
1950
|
+
const eventType = options.eventType ?? firstPositional(args);
|
|
1951
|
+
if (!eventType)
|
|
1952
|
+
throw new UsageError("Event type is required. Use: platform-todos activity event <event-type> --target-type task --target-id <id>");
|
|
1953
|
+
printOutput(await client.recordActivityEvent({
|
|
1954
|
+
target_type: requireOption(options, "targetType", "--target-type"),
|
|
1955
|
+
target_id: requireOption(options, "targetId", "--target-id"),
|
|
1956
|
+
event_type: eventType,
|
|
1957
|
+
...options.metadata ? { metadata: parseJsonObject(options.metadata, "--metadata") } : {}
|
|
1958
|
+
}), globals);
|
|
1959
|
+
return 0;
|
|
1960
|
+
}
|
|
1961
|
+
throw new UsageError("Usage: platform-todos activity <list|event>");
|
|
1962
|
+
}
|
|
1963
|
+
async function handleComments(subcommand, args, globals) {
|
|
1964
|
+
const client = new PlatformTodosClient(globals);
|
|
1965
|
+
const options = parseOptions(args);
|
|
1966
|
+
if (subcommand === "list" || !subcommand) {
|
|
1967
|
+
printOutput(await client.listComments(activityQuery(options)), globals);
|
|
1968
|
+
return 0;
|
|
1969
|
+
}
|
|
1970
|
+
if (subcommand === "add") {
|
|
1971
|
+
const body = options.body ?? firstPositional(args);
|
|
1972
|
+
if (!body)
|
|
1973
|
+
throw new UsageError("Comment body is required. Use: platform-todos comments add --target-type task --target-id <id> --body <text>");
|
|
1974
|
+
printOutput(await client.createComment({
|
|
1975
|
+
target_type: requireOption(options, "targetType", "--target-type"),
|
|
1976
|
+
target_id: requireOption(options, "targetId", "--target-id"),
|
|
1977
|
+
body,
|
|
1978
|
+
...options.metadata ? { metadata: parseJsonObject(options.metadata, "--metadata") } : {}
|
|
1979
|
+
}), globals);
|
|
1980
|
+
return 0;
|
|
1981
|
+
}
|
|
1982
|
+
throw new UsageError("Usage: platform-todos comments <list|add>");
|
|
1983
|
+
}
|
|
1984
|
+
async function handleViews(subcommand, args, globals) {
|
|
1985
|
+
const client = new PlatformTodosClient(globals);
|
|
1986
|
+
const options = parseOptions(args);
|
|
1987
|
+
if (subcommand === "list" || !subcommand) {
|
|
1988
|
+
printOutput(await client.listViews(buildQuery({
|
|
1989
|
+
limit: options.limit,
|
|
1990
|
+
cursor: options.cursor
|
|
1991
|
+
})), globals);
|
|
1992
|
+
return 0;
|
|
1993
|
+
}
|
|
1994
|
+
if (subcommand === "create") {
|
|
1995
|
+
const name = options.name ?? firstPositional(args);
|
|
1996
|
+
if (!name)
|
|
1997
|
+
throw new UsageError("View name is required.");
|
|
1998
|
+
printOutput(await client.createView({
|
|
1999
|
+
name,
|
|
2000
|
+
description: options.description,
|
|
2001
|
+
filter: options.filter ? parseJsonObject(options.filter, "--filter") : {}
|
|
2002
|
+
}), globals);
|
|
2003
|
+
return 0;
|
|
2004
|
+
}
|
|
2005
|
+
if (subcommand === "items") {
|
|
2006
|
+
const id = options.id ?? firstPositional(args);
|
|
2007
|
+
if (!id)
|
|
2008
|
+
throw new UsageError("View ID is required.");
|
|
2009
|
+
printOutput(await client.listViewItems(id, buildQuery({
|
|
2010
|
+
limit: options.limit,
|
|
2011
|
+
cursor: options.cursor
|
|
2012
|
+
})), globals);
|
|
2013
|
+
return 0;
|
|
2014
|
+
}
|
|
2015
|
+
throw new UsageError("Usage: platform-todos views <list|create|items>");
|
|
2016
|
+
}
|
|
2017
|
+
async function handleReports(subcommand, args, globals) {
|
|
2018
|
+
const kind = subcommand ?? "summary";
|
|
2019
|
+
if (kind !== "summary" && kind !== "standup" && kind !== "sprint") {
|
|
2020
|
+
throw new UsageError("Usage: platform-todos reports <summary|standup|sprint> [--format json|markdown]");
|
|
2021
|
+
}
|
|
2022
|
+
const options = parseOptions(args);
|
|
2023
|
+
const result = await new PlatformTodosClient(globals).generateReport(kind, buildQuery({
|
|
2024
|
+
format: options.format,
|
|
2025
|
+
view_id: options.view ?? options.viewId,
|
|
2026
|
+
limit: options.limit,
|
|
2027
|
+
cursor: options.cursor,
|
|
2028
|
+
types: options.types,
|
|
2029
|
+
statuses: options.statuses,
|
|
2030
|
+
labels: options.labels,
|
|
2031
|
+
project_id: options.project,
|
|
2032
|
+
assigned_to: options.assigned,
|
|
2033
|
+
priority: options.priority,
|
|
2034
|
+
ready: options.ready,
|
|
2035
|
+
blocked: options.blocked,
|
|
2036
|
+
overdue: options.overdue
|
|
2037
|
+
}));
|
|
2038
|
+
if (!globals.json && result && typeof result === "object" && result.format === "markdown") {
|
|
2039
|
+
const content = result.content;
|
|
2040
|
+
if (typeof content === "string") {
|
|
2041
|
+
printOutput(content, globals);
|
|
2042
|
+
return 0;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
printOutput(result, globals);
|
|
2046
|
+
return 0;
|
|
2047
|
+
}
|
|
2048
|
+
async function handleImport(subcommand, args, globals) {
|
|
2049
|
+
const options = parseOptions(args);
|
|
2050
|
+
if (subcommand === "issues") {
|
|
2051
|
+
const source = options.source ?? options.provider;
|
|
2052
|
+
if (!source)
|
|
2053
|
+
throw new UsageError("Issue import source is required. Use --source <github|linear|jira|csv|markdown>.");
|
|
2054
|
+
const organizationId2 = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2055
|
+
if (!organizationId2)
|
|
2056
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2057
|
+
const raw = options.file ? readFileSync3(options.file, "utf8") : options.raw;
|
|
2058
|
+
const body = {
|
|
2059
|
+
source,
|
|
2060
|
+
dryRun: options.dryRun === "false" ? false : options.apply ? false : true,
|
|
2061
|
+
conflictStrategy: options.conflictStrategy,
|
|
2062
|
+
updatedSince: options.updatedSince ?? options.since,
|
|
2063
|
+
...options.origin ? { origin: parseJsonObject(options.origin, "--origin") } : {}
|
|
2064
|
+
};
|
|
2065
|
+
if (source === "csv" || source === "markdown") {
|
|
2066
|
+
if (!raw)
|
|
2067
|
+
throw new UsageError(`${source} issue imports require --file <path> or --raw <content>.`);
|
|
2068
|
+
body.raw = raw;
|
|
2069
|
+
} else {
|
|
2070
|
+
if (!raw)
|
|
2071
|
+
throw new UsageError(`${source} issue imports require --file <json> or --raw <json>.`);
|
|
2072
|
+
const parsed = JSON.parse(raw);
|
|
2073
|
+
body.items = Array.isArray(parsed) ? parsed : parsed.items ?? parsed.issues ?? parsed.records;
|
|
2074
|
+
if (!Array.isArray(body.items))
|
|
2075
|
+
throw new UsageError(`${source} issue import JSON must be an array or contain items/issues/records.`);
|
|
2076
|
+
}
|
|
2077
|
+
printOutput(await new PlatformTodosClient(globals).importExternalIssues({ organizationId: organizationId2, body }), globals);
|
|
2078
|
+
return 0;
|
|
2079
|
+
}
|
|
2080
|
+
if (subcommand === "plan-markdown" || subcommand === "local-plan-markdown") {
|
|
2081
|
+
const artifactPath = options.path ?? options.file ?? firstPositional(args);
|
|
2082
|
+
if (!artifactPath)
|
|
2083
|
+
throw new UsageError("Plan Markdown path is required. Use --path <file-or-directory>.");
|
|
2084
|
+
const organizationId2 = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2085
|
+
if (!organizationId2)
|
|
2086
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2087
|
+
const idempotencyKey2 = options.idempotencyKey ?? options.idempotency ?? crypto.randomUUID();
|
|
2088
|
+
const dryRun2 = options.dryRun === "true" || options.preview === "true";
|
|
2089
|
+
const artifacts = readPlanMarkdownArtifacts(artifactPath);
|
|
2090
|
+
printOutput(await new PlatformTodosClient(globals).importLocalPlanMarkdown({
|
|
2091
|
+
artifacts,
|
|
2092
|
+
organizationId: organizationId2,
|
|
2093
|
+
idempotencyKey: idempotencyKey2,
|
|
2094
|
+
dryRun: dryRun2
|
|
2095
|
+
}), globals);
|
|
2096
|
+
return 0;
|
|
2097
|
+
}
|
|
2098
|
+
if (subcommand !== "local-sqlite") {
|
|
2099
|
+
throw new UsageError("Usage: platform-todos import <local-sqlite|plan-markdown|issues>");
|
|
2100
|
+
}
|
|
2101
|
+
const manifestPath = options.manifest ?? firstPositional(args);
|
|
2102
|
+
if (!manifestPath)
|
|
2103
|
+
throw new UsageError("Manifest path is required.");
|
|
2104
|
+
const organizationId = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2105
|
+
if (!organizationId)
|
|
2106
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2107
|
+
const idempotencyKey = options.idempotencyKey ?? options.idempotency ?? crypto.randomUUID();
|
|
2108
|
+
const conflictStrategy = options.conflictStrategy ?? "skip";
|
|
2109
|
+
const dryRun = options.dryRun === "true" || options.preview === "true";
|
|
2110
|
+
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
2111
|
+
const result = await new PlatformTodosClient(globals).importLocalSqliteManifest({
|
|
2112
|
+
manifest,
|
|
2113
|
+
organizationId,
|
|
2114
|
+
idempotencyKey,
|
|
2115
|
+
conflictStrategy,
|
|
2116
|
+
dryRun
|
|
2117
|
+
});
|
|
2118
|
+
printOutput(result, globals);
|
|
2119
|
+
return 0;
|
|
2120
|
+
}
|
|
2121
|
+
function readPlanMarkdownArtifacts(inputPath) {
|
|
2122
|
+
const stat = statSync(inputPath);
|
|
2123
|
+
const paths = stat.isDirectory() ? readdirSync(inputPath).filter((entry) => entry.endsWith(".md")).sort().map((entry) => `${inputPath.replace(/\/$/, "")}/${entry}`) : [inputPath];
|
|
2124
|
+
if (paths.length === 0)
|
|
2125
|
+
throw new UsageError("No .md plan artifacts found.");
|
|
2126
|
+
return paths.map((path) => ({
|
|
2127
|
+
path,
|
|
2128
|
+
markdown: readFileSync3(path, "utf8")
|
|
2129
|
+
}));
|
|
2130
|
+
}
|
|
2131
|
+
async function handlePlans(subcommand, args, globals) {
|
|
2132
|
+
const client = new PlatformTodosClient(globals);
|
|
2133
|
+
const options = parseOptions(args);
|
|
2134
|
+
if (subcommand === "templates" || subcommand === "list-templates" || !subcommand) {
|
|
2135
|
+
const organizationId2 = options.org ?? options.organizationId;
|
|
2136
|
+
printOutput(await client.listPlanTemplates(organizationId2), globals);
|
|
2137
|
+
return 0;
|
|
2138
|
+
}
|
|
2139
|
+
if (subcommand === "list") {
|
|
2140
|
+
const organizationId2 = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2141
|
+
if (!organizationId2)
|
|
2142
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2143
|
+
const query = options.includeArchived === "true" ? "?includeArchived=true" : "";
|
|
2144
|
+
printOutput(await client.listPlans(organizationId2, query), globals);
|
|
2145
|
+
return 0;
|
|
2146
|
+
}
|
|
2147
|
+
if (subcommand === "show") {
|
|
2148
|
+
const organizationId2 = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2149
|
+
if (!organizationId2)
|
|
2150
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2151
|
+
const planId = options.plan ?? options.planId ?? firstPositional(args);
|
|
2152
|
+
if (!planId)
|
|
2153
|
+
throw new UsageError("Plan ID is required. Use: platform-todos plans show <id> --org <id>");
|
|
2154
|
+
printOutput(await client.getPlan(organizationId2, planId), globals);
|
|
2155
|
+
return 0;
|
|
2156
|
+
}
|
|
2157
|
+
if (subcommand === "update") {
|
|
2158
|
+
const organizationId2 = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2159
|
+
if (!organizationId2)
|
|
2160
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2161
|
+
const planId = options.plan ?? options.planId ?? firstPositional(args);
|
|
2162
|
+
if (!planId)
|
|
2163
|
+
throw new UsageError("Plan ID is required. Use: platform-todos plans update <id> --org <id>");
|
|
2164
|
+
const objective2 = options.objective ?? secondPositional(args);
|
|
2165
|
+
const feedback = options.feedback ?? options.refinement;
|
|
2166
|
+
if (!objective2 && !feedback) {
|
|
2167
|
+
throw new UsageError("Plan update requires --objective, --feedback, or a positional objective.");
|
|
2168
|
+
}
|
|
2169
|
+
printOutput(await client.updatePlan(organizationId2, planId, stripUndefined({
|
|
2170
|
+
objective: objective2,
|
|
2171
|
+
feedback,
|
|
2172
|
+
provider: options.provider,
|
|
2173
|
+
model: options.model,
|
|
2174
|
+
projectName: options.projectName,
|
|
2175
|
+
dryRun: parseOptionalBoolean(options.dryRun),
|
|
2176
|
+
approvalBeforeCreate: parseOptionalBoolean(options.approvalBeforeCreate)
|
|
2177
|
+
})), globals);
|
|
2178
|
+
return 0;
|
|
2179
|
+
}
|
|
2180
|
+
if (subcommand === "archive") {
|
|
2181
|
+
const organizationId2 = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2182
|
+
if (!organizationId2)
|
|
2183
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2184
|
+
const planId = options.plan ?? options.planId ?? firstPositional(args);
|
|
2185
|
+
if (!planId)
|
|
2186
|
+
throw new UsageError("Plan ID is required. Use: platform-todos plans archive <id> --org <id>");
|
|
2187
|
+
printOutput(await client.archivePlan(organizationId2, planId), globals);
|
|
2188
|
+
return 0;
|
|
2189
|
+
}
|
|
2190
|
+
if (subcommand === "create") {
|
|
2191
|
+
const templateId = options.template ?? options.templateId;
|
|
2192
|
+
if (!templateId)
|
|
2193
|
+
throw new UsageError("Template ID is required. Use: platform-todos plans create --template <id>");
|
|
2194
|
+
const organizationId2 = options.org ?? options.organizationId;
|
|
2195
|
+
printOutput(await client.createPlanFromTemplate(stripUndefined({
|
|
2196
|
+
templateId,
|
|
2197
|
+
title: options.title,
|
|
2198
|
+
variables: options.variables ? parseJsonObject(options.variables, "--variables") : undefined
|
|
2199
|
+
}), organizationId2), globals);
|
|
2200
|
+
return 0;
|
|
2201
|
+
}
|
|
2202
|
+
if (subcommand !== "generate" && subcommand !== "refine") {
|
|
2203
|
+
throw new UsageError("Usage: platform-todos plans <templates|list|show|update|archive|create|generate|refine>");
|
|
2204
|
+
}
|
|
2205
|
+
const organizationId = options.org ?? options.organizationId ?? resolveAuthContext(globals).stored?.organizationId;
|
|
2206
|
+
if (!organizationId)
|
|
2207
|
+
throw new UsageError("Organization ID is required. Use --org <id>.");
|
|
2208
|
+
const objective = options.objective ?? firstPositional(args);
|
|
2209
|
+
if (!objective)
|
|
2210
|
+
throw new UsageError("Plan objective is required. Use --objective <text>.");
|
|
2211
|
+
if (subcommand === "refine" && !(options.plan ?? options.planId ?? options.revisionOf)) {
|
|
2212
|
+
throw new UsageError("Plan ID is required. Use --plan <id>.");
|
|
2213
|
+
}
|
|
2214
|
+
const body = stripUndefined({
|
|
2215
|
+
objective,
|
|
2216
|
+
provider: options.provider,
|
|
2217
|
+
model: options.model,
|
|
2218
|
+
projectName: options.projectName,
|
|
2219
|
+
dryRun: parseOptionalBoolean(options.dryRun),
|
|
2220
|
+
approvalBeforeCreate: parseOptionalBoolean(options.approvalBeforeCreate),
|
|
2221
|
+
revisionOf: options.plan ?? options.planId ?? options.revisionOf,
|
|
2222
|
+
refinement: options.feedback ?? options.refinement
|
|
2223
|
+
});
|
|
2224
|
+
printOutput(await client.generatePlan({ organizationId, body }), globals);
|
|
2225
|
+
return 0;
|
|
2226
|
+
}
|
|
2227
|
+
async function handleDocs(subcommand, args, globals) {
|
|
2228
|
+
if (subcommand !== "catalog" && subcommand !== "schema") {
|
|
2229
|
+
throw new UsageError("Usage: platform-todos docs catalog [--surface <api|cli|mcp|sdk>] [--exposure-profile <profile>]");
|
|
2230
|
+
}
|
|
2231
|
+
const options = parseOptions(args);
|
|
2232
|
+
printOutput(await new PlatformTodosClient(globals).docsCatalog(readDocsSurface(options.surface), options.exposureProfile), globals);
|
|
2233
|
+
return 0;
|
|
2234
|
+
}
|
|
2235
|
+
function readDocsSurface(value) {
|
|
2236
|
+
if (!value)
|
|
2237
|
+
return;
|
|
2238
|
+
if (value === "api" || value === "cli" || value === "mcp" || value === "sdk")
|
|
2239
|
+
return value;
|
|
2240
|
+
throw new UsageError("Docs surface must be one of: api, cli, mcp, sdk.");
|
|
2241
|
+
}
|
|
2242
|
+
function handleCompletion(shell) {
|
|
2243
|
+
if (!isCompletionShell(shell)) {
|
|
2244
|
+
throw new UsageError("Usage: platform-todos completion <bash|zsh|fish>");
|
|
2245
|
+
}
|
|
2246
|
+
console.log(renderCompletion(shell));
|
|
2247
|
+
return 0;
|
|
2248
|
+
}
|
|
2249
|
+
async function handleConfig(subcommand, args, globals) {
|
|
2250
|
+
if (!subcommand || subcommand === "show") {
|
|
2251
|
+
const auth = resolveAuthContext(globals);
|
|
2252
|
+
printOutput({
|
|
2253
|
+
profile: normalizeProfile(globals.profile ?? process.env.PLATFORM_TODOS_PROFILE ?? process.env.TODOS_PROFILE) ?? "default",
|
|
2254
|
+
apiUrl: auth.apiUrl,
|
|
2255
|
+
apiKey: redactSecret(auth.apiKey),
|
|
2256
|
+
source: auth.source,
|
|
2257
|
+
authFile: auth.source === "stored" ? "configured" : "not configured"
|
|
2258
|
+
}, globals);
|
|
2259
|
+
return 0;
|
|
2260
|
+
}
|
|
2261
|
+
if (subcommand === "set") {
|
|
2262
|
+
const options = parseOptions(args);
|
|
2263
|
+
const authFile = authFileForProfile(globals.profile);
|
|
2264
|
+
const existing = resolveAuthContext(globals).stored;
|
|
2265
|
+
writeAuthConfig({
|
|
2266
|
+
apiUrl: options.apiUrl ?? existing?.apiUrl ?? globals.apiUrl ?? DEFAULT_API_URL,
|
|
2267
|
+
apiKey: options.apiKey ?? globals.apiKey ?? existing?.apiKey,
|
|
2268
|
+
email: existing?.email,
|
|
2269
|
+
organizationId: existing?.organizationId,
|
|
2270
|
+
organizationSlug: existing?.organizationSlug,
|
|
2271
|
+
userId: existing?.userId
|
|
2272
|
+
}, authFile);
|
|
2273
|
+
printOutput({ status: "configured", profile: normalizeProfile(globals.profile) ?? "default" }, globals);
|
|
2274
|
+
return 0;
|
|
2275
|
+
}
|
|
2276
|
+
throw new UsageError("Usage: platform-todos config <show|set>");
|
|
2277
|
+
}
|
|
2278
|
+
async function handleApi(subcommand, args, globals) {
|
|
2279
|
+
if (subcommand !== "get" && subcommand !== "post") {
|
|
2280
|
+
throw new UsageError("Usage: platform-todos api <get|post> <path> [--body <json>]");
|
|
2281
|
+
}
|
|
2282
|
+
const options = parseOptions(args);
|
|
2283
|
+
const path = firstPositional(args);
|
|
2284
|
+
if (!path?.startsWith("/"))
|
|
2285
|
+
throw new UsageError("API path must start with /.");
|
|
2286
|
+
const body = options.body ? JSON.parse(options.body) : undefined;
|
|
2287
|
+
printOutput(await new PlatformTodosClient(globals).request(path, {
|
|
2288
|
+
method: subcommand.toUpperCase(),
|
|
2289
|
+
body
|
|
2290
|
+
}), globals);
|
|
2291
|
+
return 0;
|
|
2292
|
+
}
|
|
2293
|
+
async function requestOrQueue(input2) {
|
|
2294
|
+
try {
|
|
2295
|
+
return await input2.execute();
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
if (input2.queueOffline && error instanceof PlatformTodosApiError && error.status === 503) {
|
|
2298
|
+
return queuedOutput(enqueueOfflineRequest({
|
|
2299
|
+
operation: input2.operation,
|
|
2300
|
+
path: input2.path,
|
|
2301
|
+
body: input2.body,
|
|
2302
|
+
profile: input2.globals.profile,
|
|
2303
|
+
idempotencyKey: input2.idempotencyKey
|
|
2304
|
+
}));
|
|
2305
|
+
}
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
function queuedOutput(item) {
|
|
2310
|
+
return {
|
|
2311
|
+
status: "queued",
|
|
2312
|
+
id: item.id,
|
|
2313
|
+
operation: item.operation,
|
|
2314
|
+
path: item.path,
|
|
2315
|
+
idempotencyKey: item.idempotencyKey,
|
|
2316
|
+
nextAttemptAt: item.nextAttemptAt
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
function queueItemSummary(item) {
|
|
2320
|
+
return {
|
|
2321
|
+
id: item.id,
|
|
2322
|
+
operation: item.operation,
|
|
2323
|
+
path: item.path,
|
|
2324
|
+
status: item.status,
|
|
2325
|
+
attempts: item.attempts,
|
|
2326
|
+
nextAttemptAt: item.nextAttemptAt,
|
|
2327
|
+
lastError: item.lastError ?? null
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
function idempotencyHeaders(options) {
|
|
2331
|
+
const key = options.idempotencyKey ?? options.idempotency;
|
|
2332
|
+
return key ? { "Idempotency-Key": key } : {};
|
|
2333
|
+
}
|
|
2334
|
+
async function createApiKeyFromSession(client, token, tenant) {
|
|
2335
|
+
if (!token)
|
|
2336
|
+
throw new PlatformTodosApiError("Auth verification did not return a session token", 502, null);
|
|
2337
|
+
const created = await client.createApiKey(token, "platform-todos-cli", tenant);
|
|
2338
|
+
if (!created.key)
|
|
2339
|
+
throw new PlatformTodosApiError("API key creation did not return a key", 502, created);
|
|
2340
|
+
return created.key;
|
|
2341
|
+
}
|
|
2342
|
+
function selectApiKeyIfRequested(response, options, globals) {
|
|
2343
|
+
if (!readBooleanFlag(options.select) && !readBooleanFlag(options.use))
|
|
2344
|
+
return response;
|
|
2345
|
+
const key = readStringField(response, "key");
|
|
2346
|
+
if (!key) {
|
|
2347
|
+
throw new PlatformTodosApiError("API key creation did not return a key to select", 502, response);
|
|
2348
|
+
}
|
|
2349
|
+
const selected = selectApiKey(key, options, globals);
|
|
2350
|
+
return redactApiKeyResponse(response, selected);
|
|
2351
|
+
}
|
|
2352
|
+
function selectApiKey(apiKey, options, globals) {
|
|
2353
|
+
const authFile = authFileForProfile(globals.profile);
|
|
2354
|
+
const stored = readAuthConfig(authFile);
|
|
2355
|
+
const resolved = resolveAuthContext({ apiUrl: options.apiUrl ?? globals.apiUrl, profile: globals.profile });
|
|
2356
|
+
const apiUrl = options.apiUrl ?? globals.apiUrl ?? stored?.apiUrl ?? resolved.apiUrl;
|
|
2357
|
+
writeAuthConfig({
|
|
2358
|
+
apiUrl,
|
|
2359
|
+
apiKey,
|
|
2360
|
+
email: options.email ?? stored?.email,
|
|
2361
|
+
organizationId: options.organizationId ?? options.org ?? stored?.organizationId,
|
|
2362
|
+
organizationSlug: options.organizationSlug ?? stored?.organizationSlug,
|
|
2363
|
+
userId: options.userId ?? stored?.userId
|
|
2364
|
+
}, authFile);
|
|
2365
|
+
return {
|
|
2366
|
+
status: "selected",
|
|
2367
|
+
profile: selectedProfile(globals),
|
|
2368
|
+
apiUrl,
|
|
2369
|
+
apiKey: redactSecret(apiKey),
|
|
2370
|
+
email: options.email ?? stored?.email ?? null,
|
|
2371
|
+
organizationId: options.organizationId ?? options.org ?? stored?.organizationId ?? null,
|
|
2372
|
+
organization: options.organizationSlug ?? stored?.organizationSlug ?? null
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
function redactApiKeyResponse(response, selected) {
|
|
2376
|
+
if (!response || typeof response !== "object" || Array.isArray(response))
|
|
2377
|
+
return selected;
|
|
2378
|
+
const output2 = { ...response, selected };
|
|
2379
|
+
const key = readStringField(response, "key");
|
|
2380
|
+
if (key)
|
|
2381
|
+
output2.key = redactSecret(key);
|
|
2382
|
+
return output2;
|
|
2383
|
+
}
|
|
2384
|
+
function selectedProfile(globals) {
|
|
2385
|
+
return normalizeProfile(globals.profile ?? process.env.PLATFORM_TODOS_PROFILE ?? process.env.TODOS_PROFILE) ?? "default";
|
|
2386
|
+
}
|
|
2387
|
+
function openBillingSessionIfRequested(response, options) {
|
|
2388
|
+
if (!readBooleanFlag(options.open))
|
|
2389
|
+
return response;
|
|
2390
|
+
const url = readStringField(response, "url");
|
|
2391
|
+
if (!url)
|
|
2392
|
+
throw new PlatformTodosApiError("Billing session response did not include a URL to open", 502, response);
|
|
2393
|
+
openExternalUrl(url);
|
|
2394
|
+
return response && typeof response === "object" && !Array.isArray(response) ? { ...response, opened: true } : { url, opened: true };
|
|
2395
|
+
}
|
|
2396
|
+
function openExternalUrl(url) {
|
|
2397
|
+
const configuredCommand = process.env.PLATFORM_TODOS_OPEN_COMMAND;
|
|
2398
|
+
if (configuredCommand) {
|
|
2399
|
+
execFileSync(configuredCommand, [url], { stdio: "ignore" });
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2403
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2404
|
+
execFileSync(command, args, { stdio: "ignore" });
|
|
2405
|
+
}
|
|
2406
|
+
function parseArgs(argv) {
|
|
2407
|
+
const globals = { json: false, nonInteractive: false };
|
|
2408
|
+
const command = [];
|
|
2409
|
+
for (let i = 0;i < argv.length; i += 1) {
|
|
2410
|
+
const arg = argv[i];
|
|
2411
|
+
if (arg === "--json" || arg === "-j")
|
|
2412
|
+
globals.json = true;
|
|
2413
|
+
else if (arg === "--non-interactive")
|
|
2414
|
+
globals.nonInteractive = true;
|
|
2415
|
+
else if (arg === "--api-url")
|
|
2416
|
+
globals.apiUrl = requireValue(argv, ++i, arg);
|
|
2417
|
+
else if (arg === "--api-key")
|
|
2418
|
+
globals.apiKey = requireValue(argv, ++i, arg);
|
|
2419
|
+
else if (arg === "--profile")
|
|
2420
|
+
globals.profile = requireValue(argv, ++i, arg);
|
|
2421
|
+
else
|
|
2422
|
+
command.push(arg);
|
|
2423
|
+
}
|
|
2424
|
+
return { globals, command };
|
|
2425
|
+
}
|
|
2426
|
+
function parseOptions(args) {
|
|
2427
|
+
const parsed = {};
|
|
2428
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
2429
|
+
const arg = args[i];
|
|
2430
|
+
if (!arg.startsWith("--"))
|
|
2431
|
+
continue;
|
|
2432
|
+
const key = camelCase(arg.slice(2));
|
|
2433
|
+
if (!args[i + 1] || args[i + 1].startsWith("--")) {
|
|
2434
|
+
parsed[key] = "true";
|
|
2435
|
+
} else {
|
|
2436
|
+
parsed[key] = requireValue(args, ++i, arg);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return parsed;
|
|
2440
|
+
}
|
|
2441
|
+
function firstPositional(args) {
|
|
2442
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
2443
|
+
if (!args[i].startsWith("--"))
|
|
2444
|
+
return args[i];
|
|
2445
|
+
i += 1;
|
|
2446
|
+
}
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
function splitCsv(value) {
|
|
2450
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
2451
|
+
}
|
|
2452
|
+
function secondPositional(args) {
|
|
2453
|
+
let seen = 0;
|
|
2454
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
2455
|
+
if (args[i].startsWith("--")) {
|
|
2456
|
+
i += 1;
|
|
2457
|
+
continue;
|
|
2458
|
+
}
|
|
2459
|
+
seen += 1;
|
|
2460
|
+
if (seen === 2)
|
|
2461
|
+
return args[i];
|
|
2462
|
+
}
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
function requireValue(args, index, flag) {
|
|
2466
|
+
const value = args[index];
|
|
2467
|
+
if (!value || value.startsWith("--"))
|
|
2468
|
+
throw new UsageError(`${flag} requires a value.`);
|
|
2469
|
+
return value;
|
|
2470
|
+
}
|
|
2471
|
+
function buildQuery(params) {
|
|
2472
|
+
const query = new URLSearchParams;
|
|
2473
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2474
|
+
if (value)
|
|
2475
|
+
query.set(key, value);
|
|
2476
|
+
}
|
|
2477
|
+
const serialized = query.toString();
|
|
2478
|
+
return serialized ? `?${serialized}` : "";
|
|
2479
|
+
}
|
|
2480
|
+
function splitList(value) {
|
|
2481
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2482
|
+
}
|
|
2483
|
+
function parseOptionalBoolean(value) {
|
|
2484
|
+
if (value === undefined)
|
|
2485
|
+
return;
|
|
2486
|
+
if (value === "true" || value === "1" || value === "yes")
|
|
2487
|
+
return true;
|
|
2488
|
+
if (value === "false" || value === "0" || value === "no")
|
|
2489
|
+
return false;
|
|
2490
|
+
throw new UsageError(`Boolean option must be true or false, received: ${value}`);
|
|
2491
|
+
}
|
|
2492
|
+
function readBooleanFlag(value) {
|
|
2493
|
+
return value === "true" || value === "1" || value === "yes";
|
|
2494
|
+
}
|
|
2495
|
+
function stripUndefined(value) {
|
|
2496
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined));
|
|
2497
|
+
}
|
|
2498
|
+
function activityQuery(options) {
|
|
2499
|
+
return buildQuery({
|
|
2500
|
+
target_type: options.targetType,
|
|
2501
|
+
target_id: options.targetId,
|
|
2502
|
+
kind: options.kind,
|
|
2503
|
+
limit: options.limit,
|
|
2504
|
+
cursor: options.cursor
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
function requireOption(options, key, flag) {
|
|
2508
|
+
const value = options[key];
|
|
2509
|
+
if (!value)
|
|
2510
|
+
throw new UsageError(`${flag} is required.`);
|
|
2511
|
+
return value;
|
|
2512
|
+
}
|
|
2513
|
+
function parseJsonObject(value, flag) {
|
|
2514
|
+
const parsed = JSON.parse(value);
|
|
2515
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2516
|
+
throw new UsageError(`${flag} must be a JSON object.`);
|
|
2517
|
+
}
|
|
2518
|
+
return parsed;
|
|
2519
|
+
}
|
|
2520
|
+
function printOutput(data, globals) {
|
|
2521
|
+
if (globals.json || typeof data !== "string") {
|
|
2522
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2523
|
+
} else {
|
|
2524
|
+
console.log(data);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function hasBooleanOption(args, name) {
|
|
2528
|
+
return args.includes(`--${name}`);
|
|
2529
|
+
}
|
|
2530
|
+
function withoutBooleanOptions(args, names) {
|
|
2531
|
+
const flags = new Set(names.map((name) => `--${name}`));
|
|
2532
|
+
return args.filter((arg) => !flags.has(arg));
|
|
2533
|
+
}
|
|
2534
|
+
function handleError(error, globals) {
|
|
2535
|
+
const status = error instanceof PlatformTodosApiError ? error.status : 1;
|
|
2536
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2537
|
+
if (globals.json) {
|
|
2538
|
+
errorOutput.write(`${JSON.stringify({
|
|
2539
|
+
error: message,
|
|
2540
|
+
status,
|
|
2541
|
+
...error instanceof PlatformTodosApiError ? { body: error.body } : {}
|
|
2542
|
+
}, null, 2)}
|
|
2543
|
+
`);
|
|
2544
|
+
} else {
|
|
2545
|
+
console.error(message);
|
|
2546
|
+
const upgradeCommand = error instanceof PlatformTodosApiError ? readStringField(error.body, "upgradeCommand") : null;
|
|
2547
|
+
const upgradeUrl = error instanceof PlatformTodosApiError ? readStringField(error.body, "upgradeUrl") : null;
|
|
2548
|
+
const checkoutUrl = error instanceof PlatformTodosApiError ? readStringField(error.body, "checkoutUrl") : null;
|
|
2549
|
+
if (upgradeCommand || upgradeUrl || checkoutUrl) {
|
|
2550
|
+
console.error(`Upgrade: ${upgradeCommand ?? checkoutUrl ?? upgradeUrl}`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return status >= 400 && status < 600 ? 1 : status;
|
|
2554
|
+
}
|
|
2555
|
+
function readStringField(value, field) {
|
|
2556
|
+
if (!value || typeof value !== "object" || !(field in value))
|
|
2557
|
+
return null;
|
|
2558
|
+
const fieldValue = value[field];
|
|
2559
|
+
return typeof fieldValue === "string" && fieldValue.trim() ? fieldValue : null;
|
|
2560
|
+
}
|
|
2561
|
+
function normalizeEmail(email) {
|
|
2562
|
+
return email.trim().toLowerCase();
|
|
2563
|
+
}
|
|
2564
|
+
function camelCase(value) {
|
|
2565
|
+
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2566
|
+
}
|
|
2567
|
+
function isTTY() {
|
|
2568
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2569
|
+
}
|
|
2570
|
+
async function prompt(question) {
|
|
2571
|
+
const rl = createInterface({ input, output });
|
|
2572
|
+
try {
|
|
2573
|
+
return (await rl.question(question)).trim();
|
|
2574
|
+
} finally {
|
|
2575
|
+
rl.close();
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
async function promptSecret(question) {
|
|
2579
|
+
output.write(question);
|
|
2580
|
+
const echoDisabled = setTerminalEcho(false);
|
|
2581
|
+
const rl = createInterface({ input });
|
|
2582
|
+
try {
|
|
2583
|
+
return (await rl.question("")).trim();
|
|
2584
|
+
} finally {
|
|
2585
|
+
rl.close();
|
|
2586
|
+
if (echoDisabled) {
|
|
2587
|
+
setTerminalEcho(true);
|
|
2588
|
+
}
|
|
2589
|
+
output.write(`
|
|
2590
|
+
`);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function setTerminalEcho(enabled) {
|
|
2594
|
+
if (!isTTY())
|
|
2595
|
+
return false;
|
|
2596
|
+
try {
|
|
2597
|
+
execFileSync("stty", [enabled ? "echo" : "-echo"], {
|
|
2598
|
+
stdio: ["inherit", "ignore", "ignore"]
|
|
2599
|
+
});
|
|
2600
|
+
return true;
|
|
2601
|
+
} catch {
|
|
2602
|
+
return false;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
class UsageError extends Error {
|
|
2607
|
+
}
|
|
2608
|
+
if (import.meta.main) {
|
|
2609
|
+
process.exitCode = await main();
|
|
2610
|
+
}
|
|
2611
|
+
export {
|
|
2612
|
+
main
|
|
2613
|
+
};
|