@program-video/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 +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1322 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth/login.ts
|
|
7
|
+
import chalk2 from "chalk";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
|
|
10
|
+
// src/lib/auth/credentials.ts
|
|
11
|
+
import Conf from "conf";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as os from "os";
|
|
15
|
+
var CREDENTIALS_DIR = path.join(os.homedir(), ".program");
|
|
16
|
+
var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
17
|
+
function ensureCredentialsDir() {
|
|
18
|
+
if (!fs.existsSync(CREDENTIALS_DIR)) {
|
|
19
|
+
fs.mkdirSync(CREDENTIALS_DIR, { mode: 448, recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
var store = new Conf({
|
|
23
|
+
projectName: "program-cli",
|
|
24
|
+
cwd: CREDENTIALS_DIR,
|
|
25
|
+
configName: "credentials",
|
|
26
|
+
fileExtension: "json",
|
|
27
|
+
defaults: {
|
|
28
|
+
credentials: null
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
function saveCredentials(credentials) {
|
|
32
|
+
ensureCredentialsDir();
|
|
33
|
+
store.set("credentials", credentials);
|
|
34
|
+
try {
|
|
35
|
+
fs.chmodSync(CREDENTIALS_FILE, 384);
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getCredentials() {
|
|
40
|
+
return store.get("credentials");
|
|
41
|
+
}
|
|
42
|
+
function clearCredentials() {
|
|
43
|
+
store.delete("credentials");
|
|
44
|
+
}
|
|
45
|
+
function isAuthenticated() {
|
|
46
|
+
const creds = getCredentials();
|
|
47
|
+
if (!creds) return false;
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const expiresAt = creds.expiresAt * 1e3;
|
|
50
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
51
|
+
return now < expiresAt - bufferMs;
|
|
52
|
+
}
|
|
53
|
+
function needsRefresh() {
|
|
54
|
+
const creds = getCredentials();
|
|
55
|
+
if (!creds) return false;
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const expiresAt = creds.expiresAt * 1e3;
|
|
58
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
59
|
+
return now >= expiresAt - bufferMs && now < expiresAt;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/lib/auth/device.ts
|
|
63
|
+
var CLIENT_ID = "program-cli";
|
|
64
|
+
var SCOPES = ["openid", "profile", "email", "offline_access"];
|
|
65
|
+
async function requestDeviceCode(config) {
|
|
66
|
+
const url = new URL("/api/auth/device/code", config.baseUrl);
|
|
67
|
+
const response = await fetch(url.toString(), {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json"
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
client_id: CLIENT_ID,
|
|
74
|
+
scope: SCOPES.join(" ")
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorText = await response.text();
|
|
79
|
+
throw new Error(`Failed to get device code: ${response.status} ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
async function pollForToken(config, deviceCode) {
|
|
84
|
+
const url = new URL("/api/auth/device/token", config.baseUrl);
|
|
85
|
+
const response = await fetch(url.toString(), {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json"
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
92
|
+
device_code: deviceCode,
|
|
93
|
+
client_id: CLIENT_ID
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
const data = await response.json();
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
if ("error" in data) {
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Token request failed: ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
async function getUserInfo(config, accessToken) {
|
|
106
|
+
const sessionUrl = new URL("/api/auth/get-session", config.baseUrl);
|
|
107
|
+
const response = await fetch(sessionUrl.toString(), {
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${accessToken}`
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const tokenUrl = new URL("/api/auth/token", config.baseUrl);
|
|
114
|
+
const tokenResponse = await fetch(tokenUrl.toString(), {
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${accessToken}`
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
if (!tokenResponse.ok) {
|
|
120
|
+
throw new Error(`Failed to get user info: ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
const tokenData = await tokenResponse.json();
|
|
123
|
+
if (!tokenData.user?.id || !tokenData.user.email) {
|
|
124
|
+
throw new Error("Invalid token response: missing user info");
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
id: tokenData.user.id,
|
|
128
|
+
email: tokenData.user.email,
|
|
129
|
+
name: tokenData.user.name ?? tokenData.user.email,
|
|
130
|
+
organizationId: tokenData.session?.activeOrganizationId
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
if (!data.user?.id || !data.user.email) {
|
|
135
|
+
throw new Error("Invalid session response: missing user info");
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
id: data.user.id,
|
|
139
|
+
email: data.user.email,
|
|
140
|
+
name: data.user.name ?? data.user.email,
|
|
141
|
+
organizationId: data.session?.activeOrganizationId
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function startDeviceAuth(config) {
|
|
145
|
+
const deviceCode = await requestDeviceCode(config);
|
|
146
|
+
return {
|
|
147
|
+
userCode: deviceCode.user_code,
|
|
148
|
+
verificationUrl: deviceCode.verification_uri,
|
|
149
|
+
verificationUrlComplete: deviceCode.verification_uri_complete,
|
|
150
|
+
expiresIn: deviceCode.expires_in,
|
|
151
|
+
interval: deviceCode.interval,
|
|
152
|
+
pollForCompletion: async () => {
|
|
153
|
+
return pollUntilComplete(config, deviceCode);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function pollUntilComplete(config, deviceCode) {
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
const expiresAt = startTime + deviceCode.expires_in * 1e3;
|
|
160
|
+
let interval = deviceCode.interval * 1e3;
|
|
161
|
+
while (Date.now() < expiresAt) {
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
163
|
+
const result = await pollForToken(config, deviceCode.device_code);
|
|
164
|
+
if ("error" in result) {
|
|
165
|
+
switch (result.error) {
|
|
166
|
+
case "authorization_pending":
|
|
167
|
+
continue;
|
|
168
|
+
case "slow_down":
|
|
169
|
+
interval += 5e3;
|
|
170
|
+
continue;
|
|
171
|
+
case "expired_token":
|
|
172
|
+
throw new Error("Device code expired. Please start a new login.");
|
|
173
|
+
case "access_denied":
|
|
174
|
+
throw new Error("Access denied. You denied the authorization request.");
|
|
175
|
+
default:
|
|
176
|
+
throw new Error(
|
|
177
|
+
result.error_description ?? `Authorization failed: ${result.error}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const tokens = result;
|
|
182
|
+
const userInfo = await getUserInfo(config, tokens.access_token);
|
|
183
|
+
const expiresIn = tokens.expires_in ?? 3600;
|
|
184
|
+
const expiresAtTimestamp = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
185
|
+
const credentials = {
|
|
186
|
+
accessToken: tokens.access_token,
|
|
187
|
+
refreshToken: tokens.refresh_token ?? "",
|
|
188
|
+
expiresAt: expiresAtTimestamp,
|
|
189
|
+
userId: userInfo.id,
|
|
190
|
+
organizationId: userInfo.organizationId ?? "",
|
|
191
|
+
email: userInfo.email
|
|
192
|
+
};
|
|
193
|
+
saveCredentials(credentials);
|
|
194
|
+
return credentials;
|
|
195
|
+
}
|
|
196
|
+
throw new Error("Device code expired. Please start a new login.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/lib/config.ts
|
|
200
|
+
function getEnvOrDefault(key, defaultValue) {
|
|
201
|
+
return process.env[key] ?? defaultValue;
|
|
202
|
+
}
|
|
203
|
+
var BASE_URL = getEnvOrDefault(
|
|
204
|
+
"BASE_URL",
|
|
205
|
+
"https://program.video"
|
|
206
|
+
);
|
|
207
|
+
function getConvexUrl() {
|
|
208
|
+
const override = process.env.PROGRAM_CONVEX_URL;
|
|
209
|
+
if (override) return override;
|
|
210
|
+
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
211
|
+
if (convexUrl) return convexUrl;
|
|
212
|
+
return "https://mild-hyena-697.convex.cloud";
|
|
213
|
+
}
|
|
214
|
+
var CONVEX_URL = getConvexUrl();
|
|
215
|
+
var oauthConfig = {
|
|
216
|
+
baseUrl: BASE_URL
|
|
217
|
+
};
|
|
218
|
+
var convexConfig = {
|
|
219
|
+
convexUrl: CONVEX_URL,
|
|
220
|
+
oauthConfig
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/lib/output.ts
|
|
224
|
+
import chalk from "chalk";
|
|
225
|
+
function outputSuccess(data, options = {}) {
|
|
226
|
+
if (options.json) {
|
|
227
|
+
const result = { success: true, data };
|
|
228
|
+
console.log(JSON.stringify(result, null, 2));
|
|
229
|
+
} else {
|
|
230
|
+
console.log(data);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function outputError(error, options = {}) {
|
|
234
|
+
if (options.json) {
|
|
235
|
+
const result = { success: false, error };
|
|
236
|
+
console.log(JSON.stringify(result, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
console.error(chalk.red("Error:"), error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function formatTable(headers, rows, options = {}) {
|
|
242
|
+
const indent = " ".repeat(options.indent ?? 0);
|
|
243
|
+
const widths = headers.map((h, i) => {
|
|
244
|
+
const maxRow = Math.max(...rows.map((r) => (r[i] ?? "").length));
|
|
245
|
+
return Math.max(h.length, maxRow);
|
|
246
|
+
});
|
|
247
|
+
const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(widths[i] ?? 0))).join(" ");
|
|
248
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
249
|
+
const formattedRows = rows.map(
|
|
250
|
+
(row) => row.map((cell, i) => (cell || "").padEnd(widths[i] ?? 0)).join(" ")
|
|
251
|
+
);
|
|
252
|
+
return [indent + headerLine, indent + separator, ...formattedRows.map((r) => indent + r)].join(
|
|
253
|
+
"\n"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
function formatKeyValue(items, options = {}) {
|
|
257
|
+
const indent = " ".repeat(options.indent ?? 0);
|
|
258
|
+
const maxKeyLength = Math.max(...items.map((item) => item.key.length));
|
|
259
|
+
return items.map(
|
|
260
|
+
(item) => indent + chalk.dim(item.key.padEnd(maxKeyLength) + ":") + " " + item.value
|
|
261
|
+
).join("\n");
|
|
262
|
+
}
|
|
263
|
+
function formatSuccess(message2) {
|
|
264
|
+
return chalk.green("\u2713") + " " + message2;
|
|
265
|
+
}
|
|
266
|
+
function formatWarning(message2) {
|
|
267
|
+
return chalk.yellow("\u26A0") + " " + message2;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/commands/auth/login.ts
|
|
271
|
+
function formatUserCode(code) {
|
|
272
|
+
if (code.length === 8) {
|
|
273
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
274
|
+
}
|
|
275
|
+
return code;
|
|
276
|
+
}
|
|
277
|
+
async function loginCommand(options) {
|
|
278
|
+
const spinner = ora("Requesting device code...").start();
|
|
279
|
+
try {
|
|
280
|
+
const deviceAuth = await startDeviceAuth(oauthConfig);
|
|
281
|
+
spinner.stop();
|
|
282
|
+
console.log();
|
|
283
|
+
console.log(chalk2.bold(" Sign in to Program"));
|
|
284
|
+
console.log();
|
|
285
|
+
console.log(` Visit: ${chalk2.cyan(deviceAuth.verificationUrl)}`);
|
|
286
|
+
console.log();
|
|
287
|
+
console.log(` Enter code: ${chalk2.bold.yellow(formatUserCode(deviceAuth.userCode))}`);
|
|
288
|
+
console.log();
|
|
289
|
+
if (deviceAuth.verificationUrlComplete) {
|
|
290
|
+
console.log(chalk2.dim(` Or open: ${deviceAuth.verificationUrlComplete}`));
|
|
291
|
+
console.log();
|
|
292
|
+
}
|
|
293
|
+
const waitingSpinner = ora("Waiting for authorization...").start();
|
|
294
|
+
const credentials = await deviceAuth.pollForCompletion();
|
|
295
|
+
waitingSpinner.stop();
|
|
296
|
+
if (options.json) {
|
|
297
|
+
outputSuccess(
|
|
298
|
+
{
|
|
299
|
+
authenticated: true,
|
|
300
|
+
email: credentials.email,
|
|
301
|
+
userId: credentials.userId,
|
|
302
|
+
organizationId: credentials.organizationId
|
|
303
|
+
},
|
|
304
|
+
options
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
console.log(formatSuccess(`Authenticated as ${chalk2.cyan(credentials.email)}`));
|
|
308
|
+
if (credentials.organizationId) {
|
|
309
|
+
console.log(chalk2.dim(` Organization: ${credentials.organizationId}`));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const message2 = error instanceof Error ? error.message : "Authentication failed";
|
|
314
|
+
outputError(message2, options);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/commands/auth/logout.ts
|
|
320
|
+
function logoutCommand(options) {
|
|
321
|
+
const credentials = getCredentials();
|
|
322
|
+
if (!credentials) {
|
|
323
|
+
if (options.json) {
|
|
324
|
+
outputSuccess({ loggedOut: true, wasAuthenticated: false }, options);
|
|
325
|
+
} else {
|
|
326
|
+
console.log("Not currently logged in.");
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const email = credentials.email;
|
|
331
|
+
clearCredentials();
|
|
332
|
+
if (options.json) {
|
|
333
|
+
outputSuccess({ loggedOut: true, wasAuthenticated: true, email }, options);
|
|
334
|
+
} else {
|
|
335
|
+
console.log(formatSuccess(`Logged out from ${email}`));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/commands/auth/status.ts
|
|
340
|
+
import chalk3 from "chalk";
|
|
341
|
+
function statusCommand(options) {
|
|
342
|
+
const credentials = getCredentials();
|
|
343
|
+
if (!credentials) {
|
|
344
|
+
if (options.json) {
|
|
345
|
+
outputSuccess({ authenticated: false }, options);
|
|
346
|
+
} else {
|
|
347
|
+
console.log("Not logged in. Run 'program auth login' to authenticate.");
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const authenticated = isAuthenticated();
|
|
352
|
+
const expiresAt = credentials.expiresAt;
|
|
353
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
354
|
+
const expiresIn = expiresAt - now;
|
|
355
|
+
if (options.json) {
|
|
356
|
+
outputSuccess(
|
|
357
|
+
{
|
|
358
|
+
authenticated,
|
|
359
|
+
user: credentials.email,
|
|
360
|
+
userId: credentials.userId,
|
|
361
|
+
organizationId: credentials.organizationId,
|
|
362
|
+
expiresAt,
|
|
363
|
+
expiresIn,
|
|
364
|
+
needsRefresh: needsRefresh()
|
|
365
|
+
},
|
|
366
|
+
options
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
if (!authenticated) {
|
|
370
|
+
console.log(formatWarning("Token expired. Run 'program auth login' to re-authenticate."));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
console.log(formatSuccess("Logged in"));
|
|
374
|
+
console.log();
|
|
375
|
+
console.log(
|
|
376
|
+
formatKeyValue([
|
|
377
|
+
{ key: "Email", value: credentials.email },
|
|
378
|
+
{ key: "User ID", value: credentials.userId },
|
|
379
|
+
{ key: "Organization", value: credentials.organizationId || chalk3.dim("(none)") },
|
|
380
|
+
{ key: "Token expires", value: formatExpiresIn(expiresIn) }
|
|
381
|
+
])
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function formatExpiresIn(seconds) {
|
|
386
|
+
if (seconds <= 0) {
|
|
387
|
+
return chalk3.red("expired");
|
|
388
|
+
}
|
|
389
|
+
if (seconds < 60) {
|
|
390
|
+
return chalk3.yellow(`in ${seconds} seconds`);
|
|
391
|
+
}
|
|
392
|
+
if (seconds < 3600) {
|
|
393
|
+
const minutes2 = Math.floor(seconds / 60);
|
|
394
|
+
return chalk3.green(`in ${minutes2} minute${minutes2 === 1 ? "" : "s"}`);
|
|
395
|
+
}
|
|
396
|
+
const hours = Math.floor(seconds / 3600);
|
|
397
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
398
|
+
return chalk3.green(`in ${hours}h ${minutes}m`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/commands/project/create.ts
|
|
402
|
+
import chalk4 from "chalk";
|
|
403
|
+
import ora2 from "ora";
|
|
404
|
+
|
|
405
|
+
// src/lib/auth/oauth.ts
|
|
406
|
+
import open from "open";
|
|
407
|
+
|
|
408
|
+
// src/lib/auth/pkce.ts
|
|
409
|
+
import * as crypto from "crypto";
|
|
410
|
+
|
|
411
|
+
// src/lib/auth/callback-server.ts
|
|
412
|
+
import * as http from "http";
|
|
413
|
+
import { URL as URL2 } from "url";
|
|
414
|
+
|
|
415
|
+
// src/lib/auth/oauth.ts
|
|
416
|
+
var CALLBACK_PORT = 8976;
|
|
417
|
+
var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
418
|
+
var CLIENT_ID2 = "program-cli";
|
|
419
|
+
async function refreshAccessToken(config) {
|
|
420
|
+
const creds = getCredentials();
|
|
421
|
+
if (!creds?.refreshToken) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const tokenUrl = new URL("/api/auth/oauth2/token", config.baseUrl);
|
|
425
|
+
const body = new URLSearchParams({
|
|
426
|
+
grant_type: "refresh_token",
|
|
427
|
+
client_id: CLIENT_ID2,
|
|
428
|
+
refresh_token: creds.refreshToken
|
|
429
|
+
});
|
|
430
|
+
const response = await fetch(tokenUrl.toString(), {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: {
|
|
433
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
434
|
+
},
|
|
435
|
+
body: body.toString()
|
|
436
|
+
});
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const tokens = await response.json();
|
|
441
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + tokens.expires_in;
|
|
442
|
+
const newCredentials = {
|
|
443
|
+
...creds,
|
|
444
|
+
accessToken: tokens.access_token,
|
|
445
|
+
refreshToken: tokens.refresh_token ?? creds.refreshToken,
|
|
446
|
+
expiresAt
|
|
447
|
+
};
|
|
448
|
+
saveCredentials(newCredentials);
|
|
449
|
+
return newCredentials;
|
|
450
|
+
}
|
|
451
|
+
async function ensureValidToken(config) {
|
|
452
|
+
const creds = getCredentials();
|
|
453
|
+
if (!creds) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
const expiresAt = creds.expiresAt * 1e3;
|
|
458
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
459
|
+
if (now < expiresAt - bufferMs) {
|
|
460
|
+
return creds.accessToken;
|
|
461
|
+
}
|
|
462
|
+
const refreshed = await refreshAccessToken(config);
|
|
463
|
+
return refreshed?.accessToken ?? null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/lib/convex.ts
|
|
467
|
+
function createConvexClient(config) {
|
|
468
|
+
const { convexUrl } = config;
|
|
469
|
+
const gatewayUrl = convexUrl.replace(".convex.cloud", ".convex.site");
|
|
470
|
+
async function query(functionPath, args = {}) {
|
|
471
|
+
return callGateway(functionPath, args);
|
|
472
|
+
}
|
|
473
|
+
async function mutation(functionPath, args = {}) {
|
|
474
|
+
return callGateway(functionPath, args);
|
|
475
|
+
}
|
|
476
|
+
async function action(functionPath, args = {}) {
|
|
477
|
+
return callGateway(functionPath, args);
|
|
478
|
+
}
|
|
479
|
+
async function callGateway(functionPath, args) {
|
|
480
|
+
const accessToken = await ensureValidToken(config.oauthConfig);
|
|
481
|
+
if (!accessToken) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
error: "Not authenticated. Run 'program auth login' first."
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const url = new URL("/cli", gatewayUrl);
|
|
488
|
+
try {
|
|
489
|
+
const response = await fetch(url.toString(), {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
Authorization: `Bearer ${accessToken}`
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
fn: functionPath,
|
|
497
|
+
params: args
|
|
498
|
+
})
|
|
499
|
+
});
|
|
500
|
+
const result = await response.json();
|
|
501
|
+
if (!response.ok || !result.success) {
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
error: result.error ?? `Gateway error: ${response.status}`
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return { success: true, data: result.data };
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return {
|
|
510
|
+
success: false,
|
|
511
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function getAuth() {
|
|
516
|
+
return getCredentials();
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
query,
|
|
520
|
+
mutation,
|
|
521
|
+
action,
|
|
522
|
+
getAuth
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/commands/project/create.ts
|
|
527
|
+
async function createCommand(options) {
|
|
528
|
+
const spinner = options.json ? null : ora2("Creating project...").start();
|
|
529
|
+
const client = createConvexClient(convexConfig);
|
|
530
|
+
const result = await client.mutation("projects.create", {
|
|
531
|
+
title: options.title,
|
|
532
|
+
description: options.description
|
|
533
|
+
});
|
|
534
|
+
spinner?.stop();
|
|
535
|
+
if (!result.success || !result.data) {
|
|
536
|
+
outputError(result.error ?? "Failed to create project", options);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
const projectData = {
|
|
540
|
+
id: result.data,
|
|
541
|
+
title: options.title ?? "Untitled Project"
|
|
542
|
+
};
|
|
543
|
+
if (options.json) {
|
|
544
|
+
outputSuccess(projectData, options);
|
|
545
|
+
} else {
|
|
546
|
+
console.log(formatSuccess(`Created project ${chalk4.cyan(projectData.title)}`));
|
|
547
|
+
console.log(chalk4.dim(` ID: ${projectData.id}`));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/commands/project/get.ts
|
|
552
|
+
import chalk5 from "chalk";
|
|
553
|
+
import ora3 from "ora";
|
|
554
|
+
async function getCommand(projectId, options) {
|
|
555
|
+
const spinner = options.json ? null : ora3("Fetching project...").start();
|
|
556
|
+
const client = createConvexClient(convexConfig);
|
|
557
|
+
const result = await client.query("projects.byId", {
|
|
558
|
+
id: projectId
|
|
559
|
+
});
|
|
560
|
+
spinner?.stop();
|
|
561
|
+
if (!result.success || !result.data) {
|
|
562
|
+
outputError(result.error ?? "Failed to get project", options);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
const project2 = result.data;
|
|
566
|
+
if (options.json) {
|
|
567
|
+
outputSuccess(project2, options);
|
|
568
|
+
} else {
|
|
569
|
+
console.log(chalk5.bold(project2.title || "Untitled Project"));
|
|
570
|
+
if (project2.description) {
|
|
571
|
+
console.log(chalk5.dim(project2.description));
|
|
572
|
+
}
|
|
573
|
+
console.log();
|
|
574
|
+
console.log(
|
|
575
|
+
formatKeyValue([
|
|
576
|
+
{ key: "ID", value: project2._id },
|
|
577
|
+
{ key: "Created", value: new Date(project2.createdAt).toLocaleString() },
|
|
578
|
+
{ key: "Updated", value: new Date(project2.updatedAt).toLocaleString() }
|
|
579
|
+
])
|
|
580
|
+
);
|
|
581
|
+
if (project2.latestVersion) {
|
|
582
|
+
console.log(chalk5.bold("\nVersion"));
|
|
583
|
+
console.log(
|
|
584
|
+
formatKeyValue([
|
|
585
|
+
{ key: "Version", value: String(project2.latestVersion.version) },
|
|
586
|
+
{ key: "Composition ID", value: project2.latestVersion.compositionId }
|
|
587
|
+
])
|
|
588
|
+
);
|
|
589
|
+
} else {
|
|
590
|
+
console.log(chalk5.dim("\nNo versions yet"));
|
|
591
|
+
}
|
|
592
|
+
if (project2.composition) {
|
|
593
|
+
console.log(chalk5.bold("\nComposition"));
|
|
594
|
+
console.log(
|
|
595
|
+
formatKeyValue([
|
|
596
|
+
{ key: "Title", value: project2.composition.title },
|
|
597
|
+
{ key: "Resolution", value: `${project2.composition.width}x${project2.composition.height}` },
|
|
598
|
+
{ key: "FPS", value: String(project2.composition.fps) },
|
|
599
|
+
{ key: "Duration", value: `${(project2.composition.duration / 1e3).toFixed(1)}s` },
|
|
600
|
+
{ key: "Scenes", value: String(project2.composition.sceneCount) }
|
|
601
|
+
])
|
|
602
|
+
);
|
|
603
|
+
if (project2.composition.sceneCount > 0 && Array.isArray(project2.composition.scenes)) {
|
|
604
|
+
console.log(chalk5.bold("\nScenes"));
|
|
605
|
+
for (const scene of project2.composition.scenes) {
|
|
606
|
+
console.log(chalk5.dim(` - ${scene.type} (${(scene.duration / 1e3).toFixed(1)}s)`));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
console.log(chalk5.dim("\nNo composition yet"));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/commands/project/list.ts
|
|
616
|
+
import chalk6 from "chalk";
|
|
617
|
+
import ora4 from "ora";
|
|
618
|
+
async function listCommand(options) {
|
|
619
|
+
const spinner = options.json ? null : ora4("Fetching projects...").start();
|
|
620
|
+
const client = createConvexClient(convexConfig);
|
|
621
|
+
const result = await client.query("projects.list", {
|
|
622
|
+
limit: options.limit
|
|
623
|
+
});
|
|
624
|
+
spinner?.stop();
|
|
625
|
+
if (!result.success || !result.data) {
|
|
626
|
+
outputError(result.error ?? "Failed to list projects", options);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
const projects = result.data.items;
|
|
630
|
+
if (options.json) {
|
|
631
|
+
outputSuccess(projects, options);
|
|
632
|
+
} else {
|
|
633
|
+
if (projects.length === 0) {
|
|
634
|
+
console.log(chalk6.dim("No projects found. Create one with 'program project create'"));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
console.log(`Found ${chalk6.cyan(projects.length)} project${projects.length === 1 ? "" : "s"}:
|
|
638
|
+
`);
|
|
639
|
+
const rows = projects.map((p) => [
|
|
640
|
+
p._id,
|
|
641
|
+
p.title || chalk6.dim("Untitled"),
|
|
642
|
+
formatDate(p.createdAt)
|
|
643
|
+
]);
|
|
644
|
+
console.log(formatTable(["ID", "Title", "Created"], rows));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function formatDate(timestamp) {
|
|
648
|
+
const date = new Date(timestamp);
|
|
649
|
+
const now = /* @__PURE__ */ new Date();
|
|
650
|
+
const diffMs = now.getTime() - date.getTime();
|
|
651
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
652
|
+
if (diffDays === 0) {
|
|
653
|
+
return "today";
|
|
654
|
+
} else if (diffDays === 1) {
|
|
655
|
+
return "yesterday";
|
|
656
|
+
} else if (diffDays < 7) {
|
|
657
|
+
return `${diffDays} days ago`;
|
|
658
|
+
} else {
|
|
659
|
+
return date.toLocaleDateString();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/commands/workflow/list.ts
|
|
664
|
+
import chalk7 from "chalk";
|
|
665
|
+
import ora5 from "ora";
|
|
666
|
+
async function listCommand2(options) {
|
|
667
|
+
const spinner = options.json ? null : ora5("Fetching workflow templates...").start();
|
|
668
|
+
const client = createConvexClient(convexConfig);
|
|
669
|
+
const result = await client.query("workflows.listTemplates", {
|
|
670
|
+
category: options.publicOnly ? "public" : void 0
|
|
671
|
+
});
|
|
672
|
+
spinner?.stop();
|
|
673
|
+
if (!result.success || !result.data) {
|
|
674
|
+
outputError(result.error ?? "Failed to list templates", options);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
const templates = result.data;
|
|
678
|
+
if (options.json) {
|
|
679
|
+
outputSuccess(
|
|
680
|
+
templates.map((t) => ({
|
|
681
|
+
id: t._id,
|
|
682
|
+
name: t.name,
|
|
683
|
+
description: t.description,
|
|
684
|
+
public: t.public
|
|
685
|
+
})),
|
|
686
|
+
options
|
|
687
|
+
);
|
|
688
|
+
} else {
|
|
689
|
+
if (templates.length === 0) {
|
|
690
|
+
console.log(chalk7.dim("No workflow templates found."));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
console.log(`Found ${chalk7.cyan(templates.length)} template${templates.length === 1 ? "" : "s"}:
|
|
694
|
+
`);
|
|
695
|
+
const rows = templates.map((t) => [
|
|
696
|
+
t._id,
|
|
697
|
+
t.name,
|
|
698
|
+
t.description?.substring(0, 40) ?? chalk7.dim("No description"),
|
|
699
|
+
t.public ? chalk7.green("public") : chalk7.dim("private")
|
|
700
|
+
]);
|
|
701
|
+
console.log(formatTable(["ID", "Name", "Description", "Visibility"], rows));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/commands/workflow/run.ts
|
|
706
|
+
import chalk8 from "chalk";
|
|
707
|
+
import ora6 from "ora";
|
|
708
|
+
async function runCommand(templateId, options) {
|
|
709
|
+
const spinner = options.json ? null : ora6("Starting workflow execution...").start();
|
|
710
|
+
let input;
|
|
711
|
+
if (options.input) {
|
|
712
|
+
try {
|
|
713
|
+
input = JSON.parse(options.input);
|
|
714
|
+
} catch {
|
|
715
|
+
spinner?.stop();
|
|
716
|
+
outputError("Invalid JSON in --input parameter", options);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const client = createConvexClient(convexConfig);
|
|
721
|
+
const result = await client.mutation("workflows.run", {
|
|
722
|
+
templateId,
|
|
723
|
+
projectId: options.project,
|
|
724
|
+
input
|
|
725
|
+
});
|
|
726
|
+
spinner?.stop();
|
|
727
|
+
if (!result.success || !result.data) {
|
|
728
|
+
outputError(result.error ?? "Failed to start workflow", options);
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
const executionId = result.data;
|
|
732
|
+
if (options.json) {
|
|
733
|
+
outputSuccess({ executionId }, options);
|
|
734
|
+
} else {
|
|
735
|
+
console.log(formatSuccess(`Workflow started`));
|
|
736
|
+
console.log(chalk8.dim(` Execution ID: ${executionId}`));
|
|
737
|
+
console.log(chalk8.dim(` Check status with: program workflow status ${executionId}`));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/commands/workflow/status.ts
|
|
742
|
+
import chalk9 from "chalk";
|
|
743
|
+
import ora7 from "ora";
|
|
744
|
+
async function statusCommand2(executionId, options) {
|
|
745
|
+
const client = createConvexClient(convexConfig);
|
|
746
|
+
const pollInterval = options.interval ?? 2e3;
|
|
747
|
+
if (options.watch) {
|
|
748
|
+
await watchStatus(client, executionId, pollInterval, options);
|
|
749
|
+
} else {
|
|
750
|
+
await getStatusOnce(client, executionId, options);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function getStatusOnce(client, executionId, options) {
|
|
754
|
+
const spinner = options.json ? null : ora7("Fetching execution status...").start();
|
|
755
|
+
const result = await client.query("workflows.status", {
|
|
756
|
+
executionId
|
|
757
|
+
});
|
|
758
|
+
spinner?.stop();
|
|
759
|
+
if (!result.success) {
|
|
760
|
+
outputError(result.error ?? "Failed to get status", options);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
const status = result.data;
|
|
764
|
+
if (!status) {
|
|
765
|
+
outputError("Execution not found", options);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
if (options.json) {
|
|
769
|
+
outputSuccess(status, options);
|
|
770
|
+
} else {
|
|
771
|
+
printStatus(status);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function watchStatus(client, executionId, pollInterval, options) {
|
|
775
|
+
let lastStatus = null;
|
|
776
|
+
const poll = async () => {
|
|
777
|
+
const result = await client.query("workflows.status", {
|
|
778
|
+
executionId
|
|
779
|
+
});
|
|
780
|
+
if (!result.success) {
|
|
781
|
+
outputError(result.error ?? "Failed to get status", options);
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
const status = result.data;
|
|
785
|
+
if (!status) {
|
|
786
|
+
outputError("Execution not found", options);
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
if (options.json) {
|
|
790
|
+
if (status.status !== lastStatus) {
|
|
791
|
+
console.log(JSON.stringify(status));
|
|
792
|
+
lastStatus = status.status;
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
if (status.status !== lastStatus) {
|
|
796
|
+
console.clear();
|
|
797
|
+
console.log(chalk9.dim(`Watching execution ${executionId}...
|
|
798
|
+
`));
|
|
799
|
+
printStatus(status);
|
|
800
|
+
lastStatus = status.status;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (["success", "error", "cancelled"].includes(status.status)) {
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
return false;
|
|
807
|
+
};
|
|
808
|
+
let done = await poll();
|
|
809
|
+
while (!done) {
|
|
810
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
811
|
+
done = await poll();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function printStatus(status) {
|
|
815
|
+
const statusColor = getStatusColor(status.status);
|
|
816
|
+
console.log(
|
|
817
|
+
formatKeyValue([
|
|
818
|
+
{ key: "Status", value: statusColor(status.status) },
|
|
819
|
+
...status.completedAt ? [{ key: "Completed", value: new Date(status.completedAt).toLocaleString() }] : []
|
|
820
|
+
])
|
|
821
|
+
);
|
|
822
|
+
if (status.nodeStatuses && status.nodeStatuses.length > 0) {
|
|
823
|
+
console.log(chalk9.dim("\nStep progress:"));
|
|
824
|
+
for (const node of status.nodeStatuses) {
|
|
825
|
+
const nodeColor = getStatusColor(node.status);
|
|
826
|
+
console.log(` ${nodeColor(getStatusIcon(node.status))} ${node.nodeId}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (status.error) {
|
|
830
|
+
console.log(formatWarning(`
|
|
831
|
+
Error: ${status.error}`));
|
|
832
|
+
}
|
|
833
|
+
if (status.status === "success" && status.output) {
|
|
834
|
+
console.log(formatSuccess("\nOutput:"));
|
|
835
|
+
console.log(JSON.stringify(status.output, null, 2));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function getStatusColor(status) {
|
|
839
|
+
switch (status) {
|
|
840
|
+
case "success":
|
|
841
|
+
return chalk9.green;
|
|
842
|
+
case "error":
|
|
843
|
+
return chalk9.red;
|
|
844
|
+
case "cancelled":
|
|
845
|
+
return chalk9.yellow;
|
|
846
|
+
case "running":
|
|
847
|
+
return chalk9.blue;
|
|
848
|
+
case "pending":
|
|
849
|
+
default:
|
|
850
|
+
return chalk9.dim;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function getStatusIcon(status) {
|
|
854
|
+
switch (status) {
|
|
855
|
+
case "success":
|
|
856
|
+
return "\u2713";
|
|
857
|
+
case "error":
|
|
858
|
+
return "\u2715";
|
|
859
|
+
case "running":
|
|
860
|
+
return "\u25CF";
|
|
861
|
+
case "pending":
|
|
862
|
+
default:
|
|
863
|
+
return "\u25CB";
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/commands/tool/execute.ts
|
|
868
|
+
import chalk10 from "chalk";
|
|
869
|
+
import ora8 from "ora";
|
|
870
|
+
|
|
871
|
+
// src/lib/agent-detect.ts
|
|
872
|
+
function detectAgentSource() {
|
|
873
|
+
if (process.env.CLAUDECODE === "1") {
|
|
874
|
+
return "claude-code";
|
|
875
|
+
}
|
|
876
|
+
return void 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/commands/tool/execute.ts
|
|
880
|
+
async function executeCommand(toolName, options) {
|
|
881
|
+
const spinner = options.json ? null : ora8(`Executing ${toolName}...`).start();
|
|
882
|
+
const accessToken = await ensureValidToken(oauthConfig);
|
|
883
|
+
if (!accessToken) {
|
|
884
|
+
spinner?.stop();
|
|
885
|
+
outputError("Not authenticated. Run 'program auth login' first.", options);
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
let params = {};
|
|
889
|
+
if (options.params) {
|
|
890
|
+
try {
|
|
891
|
+
params = JSON.parse(options.params);
|
|
892
|
+
} catch {
|
|
893
|
+
spinner?.stop();
|
|
894
|
+
outputError("Invalid JSON in --params", options);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const source = detectAgentSource();
|
|
899
|
+
try {
|
|
900
|
+
const response = await fetch(`${BASE_URL}/api/cli/tool`, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: {
|
|
903
|
+
"Content-Type": "application/json",
|
|
904
|
+
Authorization: `Bearer ${accessToken}`
|
|
905
|
+
},
|
|
906
|
+
body: JSON.stringify({
|
|
907
|
+
projectId: options.project,
|
|
908
|
+
tool: toolName,
|
|
909
|
+
params,
|
|
910
|
+
source
|
|
911
|
+
})
|
|
912
|
+
});
|
|
913
|
+
const result = await response.json();
|
|
914
|
+
spinner?.stop();
|
|
915
|
+
if (!response.ok || !result.success) {
|
|
916
|
+
outputError(result.error ?? `Tool execution failed: ${response.status}`, options);
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
if (options.json) {
|
|
920
|
+
outputSuccess(result.data, options);
|
|
921
|
+
} else {
|
|
922
|
+
console.log(chalk10.green(`\u2713 ${toolName} executed successfully`));
|
|
923
|
+
if (result.compositionId) {
|
|
924
|
+
console.log(chalk10.dim(` Composition: ${result.compositionId}`));
|
|
925
|
+
}
|
|
926
|
+
console.log();
|
|
927
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
928
|
+
}
|
|
929
|
+
} catch (error) {
|
|
930
|
+
spinner?.stop();
|
|
931
|
+
outputError(error instanceof Error ? error.message : "Request failed", options);
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/commands/tool/list.ts
|
|
937
|
+
import chalk11 from "chalk";
|
|
938
|
+
import ora9 from "ora";
|
|
939
|
+
async function listCommand3(options) {
|
|
940
|
+
const spinner = options.json ? null : ora9("Fetching available tools...").start();
|
|
941
|
+
try {
|
|
942
|
+
const response = await fetch(`${BASE_URL}/api/cli/tool`, {
|
|
943
|
+
method: "GET"
|
|
944
|
+
});
|
|
945
|
+
const result = await response.json();
|
|
946
|
+
spinner?.stop();
|
|
947
|
+
if (!response.ok || !result.success) {
|
|
948
|
+
outputError(result.error ?? "Failed to list tools", options);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
const tools = result.tools ?? [];
|
|
952
|
+
if (options.json) {
|
|
953
|
+
outputSuccess(tools, options);
|
|
954
|
+
} else {
|
|
955
|
+
console.log(chalk11.bold(`Available Tools (${tools.length}):
|
|
956
|
+
`));
|
|
957
|
+
for (const tool2 of tools) {
|
|
958
|
+
console.log(chalk11.cyan(` ${tool2.name}`));
|
|
959
|
+
console.log(chalk11.dim(` ${tool2.description}`));
|
|
960
|
+
if (tool2.inputs.length > 0) {
|
|
961
|
+
console.log(chalk11.dim(` Inputs: ${tool2.inputs.join(", ")}`));
|
|
962
|
+
}
|
|
963
|
+
console.log();
|
|
964
|
+
}
|
|
965
|
+
console.log(chalk11.dim("Usage: program tool exec <toolName> --project <id> --params '{...}'"));
|
|
966
|
+
}
|
|
967
|
+
} catch (error) {
|
|
968
|
+
spinner?.stop();
|
|
969
|
+
outputError(error instanceof Error ? error.message : "Request failed", options);
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/commands/message/send.ts
|
|
975
|
+
import chalk12 from "chalk";
|
|
976
|
+
import ora10 from "ora";
|
|
977
|
+
async function sendCommand(text, options) {
|
|
978
|
+
const spinner = options.json ? null : ora10("Sending message...").start();
|
|
979
|
+
const accessToken = await ensureValidToken(oauthConfig);
|
|
980
|
+
if (!accessToken) {
|
|
981
|
+
spinner?.stop();
|
|
982
|
+
outputError("Not authenticated. Run 'program auth login' first.", options);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
const role = options.role ?? "assistant";
|
|
986
|
+
const source = detectAgentSource();
|
|
987
|
+
try {
|
|
988
|
+
const response = await fetch(`${BASE_URL}/api/cli/message`, {
|
|
989
|
+
method: "POST",
|
|
990
|
+
headers: {
|
|
991
|
+
"Content-Type": "application/json",
|
|
992
|
+
Authorization: `Bearer ${accessToken}`
|
|
993
|
+
},
|
|
994
|
+
body: JSON.stringify({
|
|
995
|
+
projectId: options.project,
|
|
996
|
+
role,
|
|
997
|
+
text,
|
|
998
|
+
source
|
|
999
|
+
})
|
|
1000
|
+
});
|
|
1001
|
+
const result = await response.json();
|
|
1002
|
+
spinner?.stop();
|
|
1003
|
+
if (!response.ok || !result.success) {
|
|
1004
|
+
outputError(result.error ?? `Failed to send message: ${response.status}`, options);
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
if (options.json) {
|
|
1008
|
+
outputSuccess({ messageId: result.messageId, role, text }, options);
|
|
1009
|
+
} else {
|
|
1010
|
+
console.log(chalk12.green(`\u2713 Message sent as ${role}`));
|
|
1011
|
+
if (result.messageId) {
|
|
1012
|
+
console.log(chalk12.dim(` Message ID: ${result.messageId}`));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
spinner?.stop();
|
|
1017
|
+
outputError(error instanceof Error ? error.message : "Request failed", options);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/commands/message/list.ts
|
|
1023
|
+
import chalk13 from "chalk";
|
|
1024
|
+
import ora11 from "ora";
|
|
1025
|
+
async function listCommand4(options) {
|
|
1026
|
+
const spinner = options.json ? null : ora11("Fetching messages...").start();
|
|
1027
|
+
const accessToken = await ensureValidToken(oauthConfig);
|
|
1028
|
+
if (!accessToken) {
|
|
1029
|
+
spinner?.stop();
|
|
1030
|
+
outputError(
|
|
1031
|
+
"Not authenticated. Run 'program auth login' first.",
|
|
1032
|
+
options
|
|
1033
|
+
);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const url = new URL(`${BASE_URL}/api/cli/message`);
|
|
1038
|
+
url.searchParams.set("projectId", options.project);
|
|
1039
|
+
if (options.limit) {
|
|
1040
|
+
url.searchParams.set("limit", options.limit.toString());
|
|
1041
|
+
}
|
|
1042
|
+
const response = await fetch(url.toString(), {
|
|
1043
|
+
method: "GET",
|
|
1044
|
+
headers: {
|
|
1045
|
+
Authorization: `Bearer ${accessToken}`
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
const result = await response.json();
|
|
1049
|
+
spinner?.stop();
|
|
1050
|
+
if (!response.ok || !result.success) {
|
|
1051
|
+
outputError(
|
|
1052
|
+
result.error ?? `Failed to fetch messages: ${response.status}`,
|
|
1053
|
+
options
|
|
1054
|
+
);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
if (options.json) {
|
|
1058
|
+
outputSuccess(result, options);
|
|
1059
|
+
} else {
|
|
1060
|
+
const messages = result.messages ?? [];
|
|
1061
|
+
if (messages.length === 0) {
|
|
1062
|
+
console.log(chalk13.dim("No messages in this chat yet."));
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
console.log(chalk13.bold(`Chat Messages (${messages.length}):
|
|
1066
|
+
`));
|
|
1067
|
+
for (const msg of messages) {
|
|
1068
|
+
let roleLabel = msg.role === "user" ? "You" : "Assistant";
|
|
1069
|
+
if (msg.source) {
|
|
1070
|
+
if (msg.source === "cli:claude-code") {
|
|
1071
|
+
roleLabel = "Claude via CLI";
|
|
1072
|
+
} else if (msg.source.startsWith("cli:")) {
|
|
1073
|
+
roleLabel = `${roleLabel} via CLI`;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const textContent = msg.parts.filter((p) => p.type === "text" && !!p.text).map((p) => p.text).join("\n");
|
|
1077
|
+
const toolCalls = msg.parts.filter(
|
|
1078
|
+
(p) => p.type === "tool-invocation" || p.type === "tool-call"
|
|
1079
|
+
);
|
|
1080
|
+
const time = new Date(msg.createdAt).toLocaleTimeString();
|
|
1081
|
+
const roleColor = msg.role === "user" ? chalk13.blue : chalk13.green;
|
|
1082
|
+
console.log(roleColor(`[${time}] ${roleLabel}:`));
|
|
1083
|
+
if (textContent) {
|
|
1084
|
+
const truncated = textContent.length > 500 ? textContent.substring(0, 500) + "..." : textContent;
|
|
1085
|
+
console.log(
|
|
1086
|
+
truncated.split("\n").map((line) => ` ${line}`).join("\n")
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (toolCalls.length > 0) {
|
|
1090
|
+
console.log(
|
|
1091
|
+
chalk13.dim(` [${toolCalls.length} tool call(s)]`)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
console.log();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
spinner?.stop();
|
|
1099
|
+
outputError(
|
|
1100
|
+
error instanceof Error ? error.message : "Request failed",
|
|
1101
|
+
options
|
|
1102
|
+
);
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/commands/docs.ts
|
|
1108
|
+
var COMMAND_SCHEMA = {
|
|
1109
|
+
name: "program",
|
|
1110
|
+
version: "0.1.0",
|
|
1111
|
+
description: "CLI for interacting with the Program video creation platform",
|
|
1112
|
+
commands: {
|
|
1113
|
+
auth: {
|
|
1114
|
+
description: "Authentication commands",
|
|
1115
|
+
subcommands: {
|
|
1116
|
+
login: {
|
|
1117
|
+
description: "Authenticate with the Program platform",
|
|
1118
|
+
options: {
|
|
1119
|
+
"--json": "Output as JSON"
|
|
1120
|
+
},
|
|
1121
|
+
example: "program auth login"
|
|
1122
|
+
},
|
|
1123
|
+
logout: {
|
|
1124
|
+
description: "Log out from the Program platform",
|
|
1125
|
+
options: {
|
|
1126
|
+
"--json": "Output as JSON"
|
|
1127
|
+
},
|
|
1128
|
+
example: "program auth logout"
|
|
1129
|
+
},
|
|
1130
|
+
status: {
|
|
1131
|
+
description: "Check authentication status",
|
|
1132
|
+
options: {
|
|
1133
|
+
"--json": "Output as JSON"
|
|
1134
|
+
},
|
|
1135
|
+
example: "program auth status --json"
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
project: {
|
|
1140
|
+
description: "Project management commands",
|
|
1141
|
+
subcommands: {
|
|
1142
|
+
create: {
|
|
1143
|
+
description: "Create a new project",
|
|
1144
|
+
options: {
|
|
1145
|
+
"--title <title>": "Project title",
|
|
1146
|
+
"--description <desc>": "Project description",
|
|
1147
|
+
"--json": "Output as JSON"
|
|
1148
|
+
},
|
|
1149
|
+
example: 'program project create --title "My Video" --json'
|
|
1150
|
+
},
|
|
1151
|
+
list: {
|
|
1152
|
+
description: "List projects",
|
|
1153
|
+
options: {
|
|
1154
|
+
"--limit <n>": "Maximum number of projects to return",
|
|
1155
|
+
"--json": "Output as JSON"
|
|
1156
|
+
},
|
|
1157
|
+
example: "program project list --json"
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
workflow: {
|
|
1162
|
+
description: "Workflow template commands",
|
|
1163
|
+
subcommands: {
|
|
1164
|
+
list: {
|
|
1165
|
+
description: "List available workflow templates",
|
|
1166
|
+
options: {
|
|
1167
|
+
"--limit <n>": "Maximum number of templates to return",
|
|
1168
|
+
"--public-only": "Only show public templates",
|
|
1169
|
+
"--json": "Output as JSON"
|
|
1170
|
+
},
|
|
1171
|
+
example: "program workflow list --json"
|
|
1172
|
+
},
|
|
1173
|
+
run: {
|
|
1174
|
+
description: "Execute a workflow template",
|
|
1175
|
+
args: {
|
|
1176
|
+
templateId: "ID of the template to run"
|
|
1177
|
+
},
|
|
1178
|
+
options: {
|
|
1179
|
+
"--project <id>": "Project ID to associate with execution",
|
|
1180
|
+
"--input <json>": "JSON input for the workflow",
|
|
1181
|
+
"--json": "Output as JSON"
|
|
1182
|
+
},
|
|
1183
|
+
example: `program workflow run tmpl_abc --project proj_123 --input '{"files":["src/main.ts"]}' --json`
|
|
1184
|
+
},
|
|
1185
|
+
status: {
|
|
1186
|
+
description: "Check workflow execution status",
|
|
1187
|
+
args: {
|
|
1188
|
+
executionId: "ID of the execution to check"
|
|
1189
|
+
},
|
|
1190
|
+
options: {
|
|
1191
|
+
"--watch": "Continuously poll for status updates",
|
|
1192
|
+
"--interval <ms>": "Poll interval in milliseconds (default: 2000)",
|
|
1193
|
+
"--json": "Output as JSON"
|
|
1194
|
+
},
|
|
1195
|
+
example: "program workflow status exec_789 --watch --json"
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
docs: {
|
|
1200
|
+
description: "Display CLI documentation",
|
|
1201
|
+
options: {
|
|
1202
|
+
"--schema": "Output full command schema as JSON"
|
|
1203
|
+
},
|
|
1204
|
+
example: "program docs --schema"
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
globalOptions: {
|
|
1208
|
+
"--help": "Display help information",
|
|
1209
|
+
"--version": "Display version number"
|
|
1210
|
+
},
|
|
1211
|
+
notes: [
|
|
1212
|
+
"Always use --json flag for parseable output when integrating with agents",
|
|
1213
|
+
"Workflow inputs should be valid JSON strings",
|
|
1214
|
+
"Run 'program auth login' first to authenticate"
|
|
1215
|
+
]
|
|
1216
|
+
};
|
|
1217
|
+
function docsCommand(options) {
|
|
1218
|
+
if (options.schema ?? options.json) {
|
|
1219
|
+
outputSuccess(COMMAND_SCHEMA, { json: true });
|
|
1220
|
+
} else {
|
|
1221
|
+
printHumanDocs();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function printHumanDocs() {
|
|
1225
|
+
console.log(`
|
|
1226
|
+
Program CLI v${COMMAND_SCHEMA.version}
|
|
1227
|
+
${COMMAND_SCHEMA.description}
|
|
1228
|
+
|
|
1229
|
+
COMMANDS:
|
|
1230
|
+
|
|
1231
|
+
auth login Authenticate with the Program platform
|
|
1232
|
+
auth logout Log out from the Program platform
|
|
1233
|
+
auth status Check authentication status
|
|
1234
|
+
|
|
1235
|
+
project create Create a new project
|
|
1236
|
+
project list List projects
|
|
1237
|
+
|
|
1238
|
+
workflow list List available workflow templates
|
|
1239
|
+
workflow run <id> Execute a workflow template
|
|
1240
|
+
workflow status <id> Check workflow execution status
|
|
1241
|
+
|
|
1242
|
+
docs Display this documentation
|
|
1243
|
+
docs --schema Output full command schema as JSON
|
|
1244
|
+
|
|
1245
|
+
GLOBAL OPTIONS:
|
|
1246
|
+
|
|
1247
|
+
--json Output as JSON (recommended for agents)
|
|
1248
|
+
--help Display help information
|
|
1249
|
+
--version Display version number
|
|
1250
|
+
|
|
1251
|
+
EXAMPLES:
|
|
1252
|
+
|
|
1253
|
+
# Authenticate
|
|
1254
|
+
program auth login
|
|
1255
|
+
|
|
1256
|
+
# Create a project
|
|
1257
|
+
program project create --title "My Video" --json
|
|
1258
|
+
|
|
1259
|
+
# Run a workflow
|
|
1260
|
+
program workflow run tmpl_abc --project proj_123 --json
|
|
1261
|
+
|
|
1262
|
+
# Watch execution progress
|
|
1263
|
+
program workflow status exec_789 --watch --json
|
|
1264
|
+
|
|
1265
|
+
For agent integration, always use the --json flag.
|
|
1266
|
+
`);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/index.ts
|
|
1270
|
+
var program = new Command();
|
|
1271
|
+
program.name("program").description("CLI for the Program video creation platform").version("0.1.0");
|
|
1272
|
+
var auth = program.command("auth").description("Authentication commands");
|
|
1273
|
+
auth.command("login").description("Authenticate with the Program platform").option("--json", "Output as JSON").action(async (options) => {
|
|
1274
|
+
await loginCommand(options);
|
|
1275
|
+
});
|
|
1276
|
+
auth.command("logout").description("Log out from the Program platform").option("--json", "Output as JSON").action((options) => {
|
|
1277
|
+
logoutCommand(options);
|
|
1278
|
+
});
|
|
1279
|
+
auth.command("status").description("Check authentication status").option("--json", "Output as JSON").action((options) => {
|
|
1280
|
+
statusCommand(options);
|
|
1281
|
+
});
|
|
1282
|
+
var project = program.command("project").description("Project management commands");
|
|
1283
|
+
project.command("create").description("Create a new project").option("--title <title>", "Project title").option("--description <description>", "Project description").option("--json", "Output as JSON").action(async (options) => {
|
|
1284
|
+
await createCommand(options);
|
|
1285
|
+
});
|
|
1286
|
+
project.command("list").description("List projects").option("--limit <limit>", "Maximum number of projects to return", parseInt).option("--json", "Output as JSON").action(async (options) => {
|
|
1287
|
+
await listCommand(options);
|
|
1288
|
+
});
|
|
1289
|
+
project.command("get <id>").description("Get project details including composition").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1290
|
+
await getCommand(id, options);
|
|
1291
|
+
});
|
|
1292
|
+
var workflow = program.command("workflow").description("Workflow template commands");
|
|
1293
|
+
workflow.command("list").description("List available workflow templates").option("--limit <limit>", "Maximum number of templates to return", parseInt).option("--public-only", "Only show public templates").option("--json", "Output as JSON").action(async (options) => {
|
|
1294
|
+
await listCommand2(options);
|
|
1295
|
+
});
|
|
1296
|
+
workflow.command("run <templateId>").description("Execute a workflow template").option("--project <projectId>", "Project ID to associate with execution").option("--input <json>", "JSON input for the workflow").option("--json", "Output as JSON").action(async (templateId, options) => {
|
|
1297
|
+
await runCommand(templateId, options);
|
|
1298
|
+
});
|
|
1299
|
+
workflow.command("status <executionId>").description("Check workflow execution status").option("--watch", "Continuously poll for status updates").option("--interval <ms>", "Poll interval in milliseconds", parseInt).option("--json", "Output as JSON").action(async (executionId, options) => {
|
|
1300
|
+
await statusCommand2(executionId, options);
|
|
1301
|
+
});
|
|
1302
|
+
var tool = program.command("tool").description("Execute agent tools directly");
|
|
1303
|
+
tool.command("list").description("List available agent tools").option("--json", "Output as JSON").action(async (options) => {
|
|
1304
|
+
await listCommand3(options);
|
|
1305
|
+
});
|
|
1306
|
+
tool.command("exec <toolName>").description("Execute an agent tool").requiredOption("--project <projectId>", "Project ID to execute tool against").option("--params <json>", "JSON parameters for the tool").option("--json", "Output as JSON").action(async (toolName, options) => {
|
|
1307
|
+
await executeCommand(toolName, options);
|
|
1308
|
+
});
|
|
1309
|
+
var message = program.command("message").description("Manage messages in project chat");
|
|
1310
|
+
message.command("list").description("List messages in the project chat").requiredOption("--project <projectId>", "Project ID to list messages from").option("--limit <limit>", "Maximum number of messages to return", parseInt).option("--json", "Output as JSON").action(async (options) => {
|
|
1311
|
+
await listCommand4(options);
|
|
1312
|
+
});
|
|
1313
|
+
message.command("send <text>").description(
|
|
1314
|
+
"Log a text message to the project chat. NOTE: This only adds a message to the chat history - it does NOT trigger the AI agent. To perform actions, use 'tool exec' instead."
|
|
1315
|
+
).requiredOption("--project <projectId>", "Project ID to send message to").option("--role <role>", "Message role: user or assistant (default: assistant)").option("--json", "Output as JSON").action(async (text, options) => {
|
|
1316
|
+
await sendCommand(text, options);
|
|
1317
|
+
});
|
|
1318
|
+
program.command("docs").description("Display CLI documentation and command schema").option("--schema", "Output full command schema as JSON").option("--json", "Output as JSON").action((options) => {
|
|
1319
|
+
docsCommand(options);
|
|
1320
|
+
});
|
|
1321
|
+
program.parse();
|
|
1322
|
+
//# sourceMappingURL=index.js.map
|