@jrc03c/gt-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/dist/index.js +876 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# gt-cli
|
|
2
|
+
|
|
3
|
+
A TypeScript CLI for the [GuidedTrack](https://guidedtrack.com) web API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires Node.js >= 20 and [pnpm](https://pnpm.io/).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm build
|
|
12
|
+
pnpm link --global # makes `gt` available globally
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
Auth is resolved in this order:
|
|
18
|
+
|
|
19
|
+
1. **Environment variables**: `GT_EMAIL` and `GT_PASSWORD`
|
|
20
|
+
2. **Config file**: `email` and `password` fields in `gt.config.json`
|
|
21
|
+
3. **Interactive prompt**: falls back to prompting if neither is set
|
|
22
|
+
|
|
23
|
+
## Environment
|
|
24
|
+
|
|
25
|
+
Set `GT_ENV` to target different GuidedTrack environments:
|
|
26
|
+
|
|
27
|
+
| Value | Host |
|
|
28
|
+
|-------|------|
|
|
29
|
+
| `development` | `https://localhost:3000` |
|
|
30
|
+
| `stage` | `https://guidedtrack-stage.herokuapp.com` |
|
|
31
|
+
| `production` (default) | `https://www.guidedtrack.com` |
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
### Project workflow
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gt init # Create gt.config.json by scanning for program files
|
|
39
|
+
gt config # Print current project configuration
|
|
40
|
+
gt push # Upload all local program files to the server
|
|
41
|
+
gt push --only <name> # Push a single program
|
|
42
|
+
gt push --build # Push and build
|
|
43
|
+
gt pull # Download all program sources from the server
|
|
44
|
+
gt pull --only <name> # Pull a single program
|
|
45
|
+
gt create [names...] # Create new programs on the server
|
|
46
|
+
gt build # Compile programs and report errors
|
|
47
|
+
gt compare [args...] # Compare programs using gt-compare
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Program management
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
gt program list # List all programs
|
|
54
|
+
gt program find <query> # Search programs by name
|
|
55
|
+
gt program get <name> # Fetch program metadata (JSON)
|
|
56
|
+
gt program source <name> # Fetch program source code
|
|
57
|
+
gt program build <name> # Build a specific program
|
|
58
|
+
gt program delete <name> # Delete a program (with confirmation)
|
|
59
|
+
gt program delete <name> -y # Delete without confirmation
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Program data
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
gt program data <name> # Download program data as CSV (stdout)
|
|
66
|
+
gt program csv <name> # Alias for `program data`
|
|
67
|
+
gt program data <name> -o f.csv # Save to file
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Browser shortcuts
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gt program view <name> # Open program edit page
|
|
74
|
+
gt program preview <name> # Open program preview
|
|
75
|
+
gt program run <name> # Open program run page
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Generic API access
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gt request <path> # GET any API endpoint
|
|
82
|
+
gt request <path> -X POST -d '{"key":"val"}' # POST with JSON body
|
|
83
|
+
gt request <path> -H "X-Custom:value" # Add custom headers
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
`gt init` creates a `gt.config.json` in the current directory:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"programs": {
|
|
93
|
+
"my-program": {
|
|
94
|
+
"id": 12345,
|
|
95
|
+
"key": "abc1234"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This file is gitignored. It maps local program file names to their server-side IDs and keys.
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pnpm dev [command] [args] # Run via tsx (no build step)
|
|
107
|
+
pnpm build # Bundle to dist/
|
|
108
|
+
pnpm test # Run tests
|
|
109
|
+
pnpm test:watch # Watch mode
|
|
110
|
+
pnpm lint # ESLint
|
|
111
|
+
pnpm format # Prettier
|
|
112
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/build.ts
|
|
7
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8
|
+
import { resolve as resolve2 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/types.ts
|
|
11
|
+
var ENVIRONMENT_HOSTS = {
|
|
12
|
+
development: "https://localhost:3000",
|
|
13
|
+
stage: "https://guidedtrack-stage.herokuapp.com",
|
|
14
|
+
production: "https://www.guidedtrack.com"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/lib/api.ts
|
|
18
|
+
function buildAuthHeader(credentials) {
|
|
19
|
+
const encoded = Buffer.from(
|
|
20
|
+
`${credentials.email}:${credentials.password}`
|
|
21
|
+
).toString("base64");
|
|
22
|
+
return `Basic ${encoded}`;
|
|
23
|
+
}
|
|
24
|
+
async function apiRequest(path, options) {
|
|
25
|
+
const env = options.environment ?? getEnvironment();
|
|
26
|
+
const host = ENVIRONMENT_HOSTS[env];
|
|
27
|
+
const url = `${host}${path}`;
|
|
28
|
+
const headers = {
|
|
29
|
+
Authorization: buildAuthHeader(options.credentials),
|
|
30
|
+
...options.headers
|
|
31
|
+
};
|
|
32
|
+
if (options.body) {
|
|
33
|
+
headers["Content-Type"] = "application/json";
|
|
34
|
+
}
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method: options.method ?? "GET",
|
|
37
|
+
headers,
|
|
38
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`API request failed: ${response.status} ${response.statusText}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
async function findProgramByTitle(title, credentials, environment) {
|
|
48
|
+
const query = encodeURIComponent(title);
|
|
49
|
+
const response = await apiRequest(`/programs.json?query=${query}`, {
|
|
50
|
+
credentials,
|
|
51
|
+
environment
|
|
52
|
+
});
|
|
53
|
+
const programs = await response.json();
|
|
54
|
+
return programs.find((p) => p.name === title) ?? null;
|
|
55
|
+
}
|
|
56
|
+
async function getProgram(id, credentials, environment) {
|
|
57
|
+
try {
|
|
58
|
+
const response = await apiRequest(`/programs/${id}.json`, {
|
|
59
|
+
credentials,
|
|
60
|
+
environment
|
|
61
|
+
});
|
|
62
|
+
return await response.json();
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function findProgramByKey(key, credentials, environment) {
|
|
68
|
+
const response = await apiRequest("/programs.json", {
|
|
69
|
+
credentials,
|
|
70
|
+
environment
|
|
71
|
+
});
|
|
72
|
+
const programs = await response.json();
|
|
73
|
+
return programs.find((p) => p.key === key) ?? null;
|
|
74
|
+
}
|
|
75
|
+
async function listPrograms(credentials, environment) {
|
|
76
|
+
const response = await apiRequest("/programs.json", {
|
|
77
|
+
credentials,
|
|
78
|
+
environment
|
|
79
|
+
});
|
|
80
|
+
return await response.json();
|
|
81
|
+
}
|
|
82
|
+
async function fetchProgramSource(programId, credentials, environment) {
|
|
83
|
+
const env = environment ?? getEnvironment();
|
|
84
|
+
const host = ENVIRONMENT_HOSTS[env];
|
|
85
|
+
const response = await fetch(`${host}/programs/${programId}/edit`, {
|
|
86
|
+
headers: { Authorization: buildAuthHeader(credentials) }
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Failed to fetch program source: ${response.status} ${response.statusText}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const html = await response.text();
|
|
94
|
+
const match = html.match(
|
|
95
|
+
/<textarea[^>]*name="contents"[^>]*>([\s\S]*?)<\/textarea>/
|
|
96
|
+
);
|
|
97
|
+
if (!match) {
|
|
98
|
+
throw new Error("Could not extract program source from edit page");
|
|
99
|
+
}
|
|
100
|
+
return match[1].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
101
|
+
}
|
|
102
|
+
function getEnvironment() {
|
|
103
|
+
const env = process.env.GT_ENV ?? "production";
|
|
104
|
+
if (env !== "development" && env !== "stage" && env !== "production") {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Invalid GT_ENV: "${env}". Must be development, stage, or production.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return env;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/lib/auth.ts
|
|
113
|
+
import { createInterface } from "readline";
|
|
114
|
+
|
|
115
|
+
// src/lib/config.ts
|
|
116
|
+
import { readFile, writeFile } from "fs/promises";
|
|
117
|
+
import { resolve } from "path";
|
|
118
|
+
var CONFIG_FILENAME = "gt.config.json";
|
|
119
|
+
async function loadConfig(dir) {
|
|
120
|
+
const configPath = resolve(dir ?? process.cwd(), CONFIG_FILENAME);
|
|
121
|
+
try {
|
|
122
|
+
const raw = await readFile(configPath, "utf-8");
|
|
123
|
+
return JSON.parse(raw);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err.code === "ENOENT") {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Failed to read ${CONFIG_FILENAME}: ${err}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function saveConfig(config, dir) {
|
|
132
|
+
const configPath = resolve(dir ?? process.cwd(), CONFIG_FILENAME);
|
|
133
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/lib/auth.ts
|
|
137
|
+
function prompt(question) {
|
|
138
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
139
|
+
return new Promise((resolve7) => {
|
|
140
|
+
rl2.question(question, (answer) => {
|
|
141
|
+
rl2.close();
|
|
142
|
+
resolve7(answer);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function promptSecret(question) {
|
|
147
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
148
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
149
|
+
let muted = false;
|
|
150
|
+
process.stdout.write = ((...args) => {
|
|
151
|
+
if (muted) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return originalWrite(...args);
|
|
155
|
+
});
|
|
156
|
+
return new Promise((resolve7) => {
|
|
157
|
+
rl2.question(question, (answer) => {
|
|
158
|
+
muted = false;
|
|
159
|
+
process.stdout.write = originalWrite;
|
|
160
|
+
originalWrite("\n");
|
|
161
|
+
rl2.close();
|
|
162
|
+
resolve7(answer);
|
|
163
|
+
});
|
|
164
|
+
muted = true;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async function promptCredentials() {
|
|
168
|
+
const email = await prompt("GT email: ");
|
|
169
|
+
const password = await promptSecret("GT password: ");
|
|
170
|
+
return { email, password };
|
|
171
|
+
}
|
|
172
|
+
async function resolveCredentials() {
|
|
173
|
+
const email = process.env.GT_EMAIL;
|
|
174
|
+
const password = process.env.GT_PASSWORD;
|
|
175
|
+
if (email && password) {
|
|
176
|
+
return { email, password };
|
|
177
|
+
}
|
|
178
|
+
const config = await loadConfig();
|
|
179
|
+
if (config.email && config.password) {
|
|
180
|
+
return { email: config.email, password: config.password };
|
|
181
|
+
}
|
|
182
|
+
if (process.stdin.isTTY) {
|
|
183
|
+
return promptCredentials();
|
|
184
|
+
}
|
|
185
|
+
throw new Error(
|
|
186
|
+
"No credentials found. Provide credentials via:\n - GT_EMAIL and GT_PASSWORD environment variables\n - email and password fields in gt.config.json\n - Run in a terminal for interactive prompt"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/lib/jobs.ts
|
|
191
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
192
|
+
async function pollJob(jobId, credentials, options) {
|
|
193
|
+
const interval = options?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
194
|
+
while (true) {
|
|
195
|
+
const response = await apiRequest(`/delayed_jobs/${jobId}`, {
|
|
196
|
+
credentials,
|
|
197
|
+
environment: options?.environment
|
|
198
|
+
});
|
|
199
|
+
const job = await response.json();
|
|
200
|
+
if (job.status !== "running") {
|
|
201
|
+
return job;
|
|
202
|
+
}
|
|
203
|
+
await new Promise((resolve7) => setTimeout(resolve7, interval));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/lib/build.ts
|
|
208
|
+
async function getEmbedInfo(key, credentials, environment) {
|
|
209
|
+
const response = await apiRequest(`/programs/${key}/embed`, {
|
|
210
|
+
credentials,
|
|
211
|
+
environment
|
|
212
|
+
});
|
|
213
|
+
return await response.json();
|
|
214
|
+
}
|
|
215
|
+
async function getRunContents(runId, accessKey, credentials, environment) {
|
|
216
|
+
const response = await apiRequest(`/runs/${runId}/contents`, {
|
|
217
|
+
credentials,
|
|
218
|
+
environment,
|
|
219
|
+
headers: { "X-GuidedTrack-Access-Key": accessKey }
|
|
220
|
+
});
|
|
221
|
+
return response.json();
|
|
222
|
+
}
|
|
223
|
+
function extractErrors(contents) {
|
|
224
|
+
if (!Array.isArray(contents)) {
|
|
225
|
+
if (contents && typeof contents === "object") {
|
|
226
|
+
const errors2 = [];
|
|
227
|
+
for (const value of Object.values(
|
|
228
|
+
contents
|
|
229
|
+
)) {
|
|
230
|
+
if (value && typeof value === "object" && "metadata" in value) {
|
|
231
|
+
const metadata = value.metadata;
|
|
232
|
+
if (metadata?.errors) {
|
|
233
|
+
errors2.push(...metadata.errors);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return errors2;
|
|
238
|
+
}
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const errors = [];
|
|
242
|
+
for (const item of contents) {
|
|
243
|
+
if (item && typeof item === "object" && "metadata" in item) {
|
|
244
|
+
const metadata = item.metadata;
|
|
245
|
+
if (metadata?.errors) {
|
|
246
|
+
errors.push(...metadata.errors);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return errors;
|
|
251
|
+
}
|
|
252
|
+
async function buildProgram(name, key, credentials, environment) {
|
|
253
|
+
console.log(`>> Building project "${name}" (key: ${key})`);
|
|
254
|
+
const embed = await getEmbedInfo(key, credentials, environment);
|
|
255
|
+
const contents = await getRunContents(
|
|
256
|
+
embed.run_id,
|
|
257
|
+
embed.access_key,
|
|
258
|
+
credentials,
|
|
259
|
+
environment
|
|
260
|
+
);
|
|
261
|
+
const jobId = contents && typeof contents === "object" && !Array.isArray(contents) ? contents.job : null;
|
|
262
|
+
if (jobId && typeof jobId === "number") {
|
|
263
|
+
process.stdout.write(`>>>> Waiting for new build (job: ${jobId})... `);
|
|
264
|
+
await pollJob(jobId, credentials, { environment });
|
|
265
|
+
console.log("done");
|
|
266
|
+
} else {
|
|
267
|
+
console.log(">>>> No changes to build");
|
|
268
|
+
}
|
|
269
|
+
const result = await getRunContents(
|
|
270
|
+
embed.run_id,
|
|
271
|
+
embed.access_key,
|
|
272
|
+
credentials,
|
|
273
|
+
environment
|
|
274
|
+
);
|
|
275
|
+
const errors = extractErrors(result);
|
|
276
|
+
if (errors.length === 0) {
|
|
277
|
+
console.log(">>>> No errors");
|
|
278
|
+
} else {
|
|
279
|
+
console.log(">>>> Found compilation errors:");
|
|
280
|
+
console.log(errors.join("\n"));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/commands/build.ts
|
|
285
|
+
var PROJECTS_FILENAME = ".gt_projects";
|
|
286
|
+
function registerBuild(program2) {
|
|
287
|
+
program2.command("build").description("Compile programs and report errors").action(async () => {
|
|
288
|
+
const credentials = await resolveCredentials();
|
|
289
|
+
const environment = getEnvironment();
|
|
290
|
+
const projects = await loadProjects();
|
|
291
|
+
if (!projects) {
|
|
292
|
+
console.log(`No ${PROJECTS_FILENAME} file, nothing to build`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
for (const project of projects) {
|
|
296
|
+
const found = await findProgramByTitle(project, credentials, environment);
|
|
297
|
+
if (!found) {
|
|
298
|
+
console.error(`>> Program "${project}" not found, skipping...`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
await buildProgram(project, found.key, credentials, environment);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async function loadProjects() {
|
|
306
|
+
try {
|
|
307
|
+
const raw = await readFile2(
|
|
308
|
+
resolve2(process.cwd(), PROJECTS_FILENAME),
|
|
309
|
+
"utf-8"
|
|
310
|
+
);
|
|
311
|
+
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/commands/compare.ts
|
|
318
|
+
import { execFileSync } from "child_process";
|
|
319
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
320
|
+
function registerCompare(program2) {
|
|
321
|
+
program2.command("compare").description("Compare programs using gt-compare").allowUnknownOption().argument("[args...]", "Arguments to pass to gt-compare").action((args) => {
|
|
322
|
+
const gtPath = process.env.GT_COMPARE_PATH ?? findGtCompare();
|
|
323
|
+
if (!gtPath) {
|
|
324
|
+
console.error(
|
|
325
|
+
"Could not find gt-compare. Set GT_COMPARE_PATH to the gt-compare directory."
|
|
326
|
+
);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
execFileSync("./gt-compare", args, {
|
|
331
|
+
cwd: gtPath,
|
|
332
|
+
stdio: "inherit"
|
|
333
|
+
});
|
|
334
|
+
} catch {
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function findGtCompare() {
|
|
340
|
+
try {
|
|
341
|
+
const gtBin = execFileSync("which", ["gt"], { encoding: "utf-8" }).trim();
|
|
342
|
+
return resolve3(dirname(gtBin), "gt-compare");
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/commands/config.ts
|
|
349
|
+
function registerConfig(program2) {
|
|
350
|
+
program2.command("config").description("Print current project configuration").action(async () => {
|
|
351
|
+
const config = await loadConfig();
|
|
352
|
+
console.log(JSON.stringify(config, null, 2));
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/lib/files.ts
|
|
357
|
+
import { readdir } from "fs/promises";
|
|
358
|
+
import { join, relative } from "path";
|
|
359
|
+
async function getLocalGtFiles(dir) {
|
|
360
|
+
const files = [];
|
|
361
|
+
await scanDir(dir, dir, files);
|
|
362
|
+
return files.sort();
|
|
363
|
+
}
|
|
364
|
+
async function scanDir(root, current, results) {
|
|
365
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
const fullPath = join(current, entry.name);
|
|
368
|
+
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
369
|
+
await scanDir(root, fullPath, results);
|
|
370
|
+
} else if (entry.isFile() && entry.name.endsWith(".gt")) {
|
|
371
|
+
results.push(relative(root, fullPath));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/commands/create.ts
|
|
377
|
+
function registerCreate(program2) {
|
|
378
|
+
program2.command("create").description("Create new programs on the server").argument(
|
|
379
|
+
"[names...]",
|
|
380
|
+
"Program names to create (defaults to all .gt files in cwd)"
|
|
381
|
+
).action(async (names) => {
|
|
382
|
+
const credentials = await resolveCredentials();
|
|
383
|
+
const environment = getEnvironment();
|
|
384
|
+
const filenames = names.length > 0 ? names : await getLocalGtFiles(process.cwd());
|
|
385
|
+
if (filenames.length === 0) {
|
|
386
|
+
console.error("No .gt files to create.");
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
console.log(`Creating programs in ${environment}...`);
|
|
390
|
+
for (const filename of filenames) {
|
|
391
|
+
process.stdout.write(`>> Creating "${filename}"... `);
|
|
392
|
+
try {
|
|
393
|
+
const response = await apiRequest("/programs", {
|
|
394
|
+
method: "POST",
|
|
395
|
+
credentials,
|
|
396
|
+
environment,
|
|
397
|
+
body: { name: filename }
|
|
398
|
+
});
|
|
399
|
+
const data = await response.json();
|
|
400
|
+
if (data.job_id) {
|
|
401
|
+
console.log("done");
|
|
402
|
+
} else {
|
|
403
|
+
console.error("failed");
|
|
404
|
+
console.error(`${JSON.stringify(data)} -- skipping`);
|
|
405
|
+
}
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.error("failed");
|
|
408
|
+
console.error(e.message);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/lib/prompt.ts
|
|
416
|
+
import { createInterface as createInterface2 } from "readline";
|
|
417
|
+
var rl = () => createInterface2({ input: process.stdin, output: process.stdout });
|
|
418
|
+
async function ask(question) {
|
|
419
|
+
const iface = rl();
|
|
420
|
+
return new Promise((resolve7) => {
|
|
421
|
+
iface.question(question, (answer) => {
|
|
422
|
+
iface.close();
|
|
423
|
+
resolve7(answer.trim());
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async function confirm(question) {
|
|
428
|
+
const answer = await ask(`${question} (y/N) `);
|
|
429
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
430
|
+
}
|
|
431
|
+
async function choose(question, options) {
|
|
432
|
+
console.log(question);
|
|
433
|
+
for (let i = 0; i < options.length; i++) {
|
|
434
|
+
console.log(` ${i + 1}. ${options[i]}`);
|
|
435
|
+
}
|
|
436
|
+
const answer = await ask("Enter choice: ");
|
|
437
|
+
const choice = parseInt(answer, 10);
|
|
438
|
+
if (isNaN(choice) || choice < 1 || choice > options.length) {
|
|
439
|
+
console.log("Invalid choice.");
|
|
440
|
+
return -1;
|
|
441
|
+
}
|
|
442
|
+
return choice - 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/commands/init.ts
|
|
446
|
+
function registerInit(program2) {
|
|
447
|
+
program2.command("init").description("Create gt.config.json by linking local .gt files to programs").action(async () => {
|
|
448
|
+
const credentials = await resolveCredentials();
|
|
449
|
+
const environment = getEnvironment();
|
|
450
|
+
const existing = await loadConfig();
|
|
451
|
+
const programs = existing.programs ?? {};
|
|
452
|
+
const files = await getLocalGtFiles(process.cwd());
|
|
453
|
+
if (files.length === 0) {
|
|
454
|
+
console.log("No .gt files found.");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const linkedFiles = new Set(Object.values(programs).map((p) => p.file));
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
if (linkedFiles.has(file)) {
|
|
460
|
+
console.log(`
|
|
461
|
+
"${file}" is already linked, skipping.`);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
console.log(`
|
|
465
|
+
Found "${file}"`);
|
|
466
|
+
const shouldLink = await confirm(
|
|
467
|
+
"Do you want to link it to a program on guidedtrack.com?"
|
|
468
|
+
);
|
|
469
|
+
if (!shouldLink) continue;
|
|
470
|
+
const idType = await choose(
|
|
471
|
+
"Which identifier do you want to use to find the program?",
|
|
472
|
+
[
|
|
473
|
+
`The program's title (e.g., "My Cool Program")`,
|
|
474
|
+
"The program's ID (e.g., 12345)",
|
|
475
|
+
`The program's key (e.g., "abc1234")`
|
|
476
|
+
]
|
|
477
|
+
);
|
|
478
|
+
if (idType === -1) continue;
|
|
479
|
+
let found = null;
|
|
480
|
+
if (idType === 0) {
|
|
481
|
+
const title = await ask("Enter program title: ");
|
|
482
|
+
if (!title) continue;
|
|
483
|
+
process.stdout.write(`Looking up "${title}" in ${environment}... `);
|
|
484
|
+
found = await findProgramByTitle(title, credentials, environment);
|
|
485
|
+
} else if (idType === 1) {
|
|
486
|
+
const idStr = await ask("Enter program ID: ");
|
|
487
|
+
const id = parseInt(idStr, 10);
|
|
488
|
+
if (isNaN(id)) {
|
|
489
|
+
console.log("Invalid ID.");
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
process.stdout.write(
|
|
493
|
+
`Looking up program ${id} in ${environment}... `
|
|
494
|
+
);
|
|
495
|
+
found = await getProgram(id, credentials, environment);
|
|
496
|
+
} else {
|
|
497
|
+
const key = await ask("Enter program key: ");
|
|
498
|
+
if (!key) continue;
|
|
499
|
+
process.stdout.write(
|
|
500
|
+
`Looking up program "${key}" in ${environment}... `
|
|
501
|
+
);
|
|
502
|
+
found = await findProgramByKey(key, credentials, environment);
|
|
503
|
+
}
|
|
504
|
+
if (!found) {
|
|
505
|
+
console.log("not found.");
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
console.log(`found! ("${found.name}")`);
|
|
509
|
+
if (programs[found.key]) {
|
|
510
|
+
console.log(
|
|
511
|
+
`Program "${found.name}" is already linked to "${programs[found.key].file}", skipping.`
|
|
512
|
+
);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
programs[found.key] = { file, id: found.id };
|
|
516
|
+
linkedFiles.add(file);
|
|
517
|
+
console.log(`Linked "${file}" \u2192 "${found.name}" (key: ${found.key})`);
|
|
518
|
+
}
|
|
519
|
+
const config = { ...existing, programs };
|
|
520
|
+
await saveConfig(config);
|
|
521
|
+
console.log(`
|
|
522
|
+
Wrote ${CONFIG_FILENAME}`);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/commands/program.ts
|
|
527
|
+
import { exec } from "child_process";
|
|
528
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
529
|
+
import { resolve as resolve4 } from "path";
|
|
530
|
+
import { createInterface as createInterface3 } from "readline";
|
|
531
|
+
function registerProgram(parent) {
|
|
532
|
+
const program2 = parent.command("program").description("Manage programs on the server");
|
|
533
|
+
program2.command("list").description("List all programs").action(async () => {
|
|
534
|
+
const credentials = await resolveCredentials();
|
|
535
|
+
const environment = getEnvironment();
|
|
536
|
+
const programs = await listPrograms(credentials, environment);
|
|
537
|
+
if (programs.length === 0) {
|
|
538
|
+
console.log("No programs found.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
for (const p of programs) {
|
|
542
|
+
console.log(`${p.id} ${p.key} ${p.name}`);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
program2.command("get").description("Fetch program metadata").argument("<name>", "Program name").action(async (name) => {
|
|
546
|
+
const credentials = await resolveCredentials();
|
|
547
|
+
const environment = getEnvironment();
|
|
548
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
549
|
+
if (!found) {
|
|
550
|
+
console.error(`Program "${name}" not found.`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
console.log(JSON.stringify(found, null, 2));
|
|
554
|
+
});
|
|
555
|
+
program2.command("source").description("Fetch program source code").argument("<name>", "Program name").action(async (name) => {
|
|
556
|
+
const credentials = await resolveCredentials();
|
|
557
|
+
const environment = getEnvironment();
|
|
558
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
559
|
+
if (!found) {
|
|
560
|
+
console.error(`Program "${name}" not found.`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
const source = await fetchProgramSource(
|
|
564
|
+
found.id,
|
|
565
|
+
credentials,
|
|
566
|
+
environment
|
|
567
|
+
);
|
|
568
|
+
process.stdout.write(source);
|
|
569
|
+
});
|
|
570
|
+
program2.command("find").description("Search programs by name").argument("<query>", "Search query").action(async (query) => {
|
|
571
|
+
const credentials = await resolveCredentials();
|
|
572
|
+
const environment = getEnvironment();
|
|
573
|
+
const encoded = encodeURIComponent(query);
|
|
574
|
+
const response = await apiRequest(`/programs.json?query=${encoded}`, {
|
|
575
|
+
credentials,
|
|
576
|
+
environment
|
|
577
|
+
});
|
|
578
|
+
const programs = await response.json();
|
|
579
|
+
if (programs.length === 0) {
|
|
580
|
+
console.log("No programs found.");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
for (const p of programs) {
|
|
584
|
+
console.log(`${p.id} ${p.key} ${p.name}`);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
program2.command("delete").description("Delete a program (with confirmation)").argument("<name>", "Program name").option("-y, --yes", "Skip confirmation prompt").action(async (name, options) => {
|
|
588
|
+
const credentials = await resolveCredentials();
|
|
589
|
+
const environment = getEnvironment();
|
|
590
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
591
|
+
if (!found) {
|
|
592
|
+
console.error(`Program "${name}" not found.`);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
if (!options.yes) {
|
|
596
|
+
const confirmed = await confirm2(
|
|
597
|
+
`Delete "${name}" (id: ${found.id})? This cannot be undone. [y/N] `
|
|
598
|
+
);
|
|
599
|
+
if (!confirmed) {
|
|
600
|
+
console.log("Aborted.");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
await apiRequest(`/programs/${found.id}.json`, {
|
|
605
|
+
method: "DELETE",
|
|
606
|
+
credentials,
|
|
607
|
+
environment
|
|
608
|
+
});
|
|
609
|
+
console.log(`Deleted "${name}" (id: ${found.id})`);
|
|
610
|
+
});
|
|
611
|
+
program2.command("build").description("Build a specific program").argument("<name>", "Program name").action(async (name) => {
|
|
612
|
+
const credentials = await resolveCredentials();
|
|
613
|
+
const environment = getEnvironment();
|
|
614
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
615
|
+
if (!found) {
|
|
616
|
+
console.error(`Program "${name}" not found.`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
console.log(`Building "${name}" (key: ${found.key})...`);
|
|
620
|
+
const embed = await getEmbedInfo(found.key, credentials, environment);
|
|
621
|
+
const contents = await getRunContents(
|
|
622
|
+
embed.run_id,
|
|
623
|
+
embed.access_key,
|
|
624
|
+
credentials,
|
|
625
|
+
environment
|
|
626
|
+
);
|
|
627
|
+
const jobId = contents && typeof contents === "object" && !Array.isArray(contents) ? contents.job : null;
|
|
628
|
+
if (jobId && typeof jobId === "number") {
|
|
629
|
+
process.stdout.write(`Waiting for build (job: ${jobId})... `);
|
|
630
|
+
await pollJob(jobId, credentials, { environment });
|
|
631
|
+
console.log("done");
|
|
632
|
+
} else {
|
|
633
|
+
console.log("No changes to build");
|
|
634
|
+
}
|
|
635
|
+
const result = await getRunContents(
|
|
636
|
+
embed.run_id,
|
|
637
|
+
embed.access_key,
|
|
638
|
+
credentials,
|
|
639
|
+
environment
|
|
640
|
+
);
|
|
641
|
+
const errors = extractErrors(result);
|
|
642
|
+
if (errors.length === 0) {
|
|
643
|
+
console.log("No errors");
|
|
644
|
+
} else {
|
|
645
|
+
console.log("Found compilation errors:");
|
|
646
|
+
console.log(errors.join("\n"));
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
program2.command("view").description("Open program edit page in browser").argument("<name>", "Program name").action(async (name) => {
|
|
650
|
+
const credentials = await resolveCredentials();
|
|
651
|
+
const environment = getEnvironment();
|
|
652
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
653
|
+
if (!found) {
|
|
654
|
+
console.error(`Program "${name}" not found.`);
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
const url = `${ENVIRONMENT_HOSTS[environment]}/programs/${found.id}/edit`;
|
|
658
|
+
openBrowser(url);
|
|
659
|
+
});
|
|
660
|
+
program2.command("preview").description("Open program preview in browser").argument("<name>", "Program name").action(async (name) => {
|
|
661
|
+
const credentials = await resolveCredentials();
|
|
662
|
+
const environment = getEnvironment();
|
|
663
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
664
|
+
if (!found) {
|
|
665
|
+
console.error(`Program "${name}" not found.`);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
const url = `${ENVIRONMENT_HOSTS[environment]}/programs/${found.key}/preview`;
|
|
669
|
+
openBrowser(url);
|
|
670
|
+
});
|
|
671
|
+
program2.command("run").description("Open program run page in browser").argument("<name>", "Program name").action(async (name) => {
|
|
672
|
+
const credentials = await resolveCredentials();
|
|
673
|
+
const environment = getEnvironment();
|
|
674
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
675
|
+
if (!found) {
|
|
676
|
+
console.error(`Program "${name}" not found.`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
const url = `${ENVIRONMENT_HOSTS[environment]}/programs/${found.key}/run`;
|
|
680
|
+
openBrowser(url);
|
|
681
|
+
});
|
|
682
|
+
program2.command("data").alias("csv").description("Download program data as CSV").argument("<name>", "Program name").option("-o, --output <file>", "Save to file instead of stdout").action(async (name, options) => {
|
|
683
|
+
const credentials = await resolveCredentials();
|
|
684
|
+
const environment = getEnvironment();
|
|
685
|
+
const found = await findProgramByTitle(name, credentials, environment);
|
|
686
|
+
if (!found) {
|
|
687
|
+
console.error(`Program "${name}" not found.`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
const response = await apiRequest(`/programs/${found.id}/csv`, {
|
|
691
|
+
credentials,
|
|
692
|
+
environment
|
|
693
|
+
});
|
|
694
|
+
const csv = await response.text();
|
|
695
|
+
if (options.output) {
|
|
696
|
+
await writeFile2(resolve4(process.cwd(), options.output), csv);
|
|
697
|
+
console.log(`Saved to ${options.output}`);
|
|
698
|
+
} else {
|
|
699
|
+
process.stdout.write(csv);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function confirm2(prompt2) {
|
|
704
|
+
const rl2 = createInterface3({ input: process.stdin, output: process.stdout });
|
|
705
|
+
return new Promise((resolve7) => {
|
|
706
|
+
rl2.question(prompt2, (answer) => {
|
|
707
|
+
rl2.close();
|
|
708
|
+
resolve7(answer.toLowerCase() === "y");
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function openBrowser(url) {
|
|
713
|
+
console.log(url);
|
|
714
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
715
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/commands/pull.ts
|
|
719
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
720
|
+
import { resolve as resolve5 } from "path";
|
|
721
|
+
function registerPull(program2) {
|
|
722
|
+
program2.command("pull").description("Download program source from the server").option("-o, --only <key>", "Pull only the specified program (by key)").action(async (options) => {
|
|
723
|
+
const credentials = await resolveCredentials();
|
|
724
|
+
const environment = getEnvironment();
|
|
725
|
+
const config = await loadConfig();
|
|
726
|
+
const programs = config.programs ?? {};
|
|
727
|
+
let entries;
|
|
728
|
+
if (options.only) {
|
|
729
|
+
const ref = programs[options.only];
|
|
730
|
+
if (!ref) {
|
|
731
|
+
console.error(
|
|
732
|
+
`Program with key "${options.only}" not found in config.`
|
|
733
|
+
);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
entries = [[options.only, ref]];
|
|
737
|
+
} else {
|
|
738
|
+
entries = Object.entries(programs);
|
|
739
|
+
}
|
|
740
|
+
if (entries.length === 0) {
|
|
741
|
+
console.error("No programs in config. Run `gt init` first.");
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
console.log(`Pulling from ${environment}...`);
|
|
745
|
+
for (const [, ref] of entries) {
|
|
746
|
+
process.stdout.write(
|
|
747
|
+
`>> Downloading "${ref.file}" (id: ${ref.id})... `
|
|
748
|
+
);
|
|
749
|
+
const source = await fetchProgramSource(
|
|
750
|
+
ref.id,
|
|
751
|
+
credentials,
|
|
752
|
+
environment
|
|
753
|
+
);
|
|
754
|
+
await writeFile3(resolve5(process.cwd(), ref.file), source);
|
|
755
|
+
console.log("done");
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/commands/push.ts
|
|
761
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
762
|
+
import { resolve as resolve6 } from "path";
|
|
763
|
+
function registerPush(program2) {
|
|
764
|
+
program2.command("push").description("Upload local program files to the server").option("-o, --only <key>", "Push only the specified program (by key)").option("-b, --build", "Build after pushing").action(async (options) => {
|
|
765
|
+
const credentials = await resolveCredentials();
|
|
766
|
+
const environment = getEnvironment();
|
|
767
|
+
const config = await loadConfig();
|
|
768
|
+
const programs = config.programs ?? {};
|
|
769
|
+
let entries;
|
|
770
|
+
if (options.only) {
|
|
771
|
+
const ref = programs[options.only];
|
|
772
|
+
if (!ref) {
|
|
773
|
+
console.error(
|
|
774
|
+
`Program with key "${options.only}" not found in config.`
|
|
775
|
+
);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
entries = [[options.only, ref]];
|
|
779
|
+
} else {
|
|
780
|
+
entries = Object.entries(programs);
|
|
781
|
+
}
|
|
782
|
+
if (entries.length === 0) {
|
|
783
|
+
console.error("No programs in config. Run `gt init` first.");
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
console.log(`Pushing to ${environment}...`);
|
|
787
|
+
const pushed = [];
|
|
788
|
+
for (const [key, ref] of entries) {
|
|
789
|
+
process.stdout.write(
|
|
790
|
+
`>> Updating "${ref.file}" (id: ${ref.id})... `
|
|
791
|
+
);
|
|
792
|
+
const contents = await readFile3(
|
|
793
|
+
resolve6(process.cwd(), ref.file),
|
|
794
|
+
"utf-8"
|
|
795
|
+
);
|
|
796
|
+
await apiRequest(`/programs/${ref.id}.json`, {
|
|
797
|
+
method: "PUT",
|
|
798
|
+
credentials,
|
|
799
|
+
environment,
|
|
800
|
+
body: { contents, program: { description: "" } }
|
|
801
|
+
});
|
|
802
|
+
console.log("done");
|
|
803
|
+
pushed.push({ file: ref.file, key });
|
|
804
|
+
}
|
|
805
|
+
if (options.build && pushed.length > 0) {
|
|
806
|
+
console.log("\nBuilding pushed programs...");
|
|
807
|
+
for (const { file, key } of pushed) {
|
|
808
|
+
await buildProgram(file, key, credentials, environment);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/commands/request.ts
|
|
815
|
+
function registerRequest(program2) {
|
|
816
|
+
program2.command("request").description("Send a generic API request").argument("<path>", "API path (e.g., /programs.json)").option("-X, --method <method>", "HTTP method", "GET").option("-d, --data <json>", "Request body (JSON string)").option(
|
|
817
|
+
"-H, --header <header...>",
|
|
818
|
+
"Additional headers (key:value)"
|
|
819
|
+
).action(
|
|
820
|
+
async (path, options) => {
|
|
821
|
+
const credentials = await resolveCredentials();
|
|
822
|
+
const environment = getEnvironment();
|
|
823
|
+
const headers = {};
|
|
824
|
+
if (options.header) {
|
|
825
|
+
for (const h of options.header) {
|
|
826
|
+
const idx = h.indexOf(":");
|
|
827
|
+
if (idx === -1) {
|
|
828
|
+
console.error(`Invalid header format: "${h}" (expected key:value)`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
let body;
|
|
835
|
+
if (options.data) {
|
|
836
|
+
try {
|
|
837
|
+
body = JSON.parse(options.data);
|
|
838
|
+
} catch {
|
|
839
|
+
console.error("Invalid JSON body");
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const response = await apiRequest(path, {
|
|
844
|
+
method: options.method,
|
|
845
|
+
credentials,
|
|
846
|
+
environment,
|
|
847
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
848
|
+
body
|
|
849
|
+
});
|
|
850
|
+
const text = await response.text();
|
|
851
|
+
try {
|
|
852
|
+
const json = JSON.parse(text);
|
|
853
|
+
console.log(JSON.stringify(json, null, 2));
|
|
854
|
+
} catch {
|
|
855
|
+
console.log(text);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/index.ts
|
|
862
|
+
var program = new Command();
|
|
863
|
+
program.name("gt").description("CLI for the GuidedTrack web API").version("0.1.0");
|
|
864
|
+
registerPush(program);
|
|
865
|
+
registerPull(program);
|
|
866
|
+
registerCreate(program);
|
|
867
|
+
registerBuild(program);
|
|
868
|
+
registerCompare(program);
|
|
869
|
+
registerInit(program);
|
|
870
|
+
registerConfig(program);
|
|
871
|
+
registerProgram(program);
|
|
872
|
+
registerRequest(program);
|
|
873
|
+
program.parseAsync().catch((err) => {
|
|
874
|
+
console.error(err.message);
|
|
875
|
+
process.exit(1);
|
|
876
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"gt": "dist/index.js"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"commander": "^13.1.0"
|
|
7
|
+
},
|
|
8
|
+
"description": "CLI for the GuidedTrack web API",
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@eslint/css": "^0.14.1",
|
|
11
|
+
"@eslint/js": "^9.12.0",
|
|
12
|
+
"@eslint/json": "^0.14.0",
|
|
13
|
+
"@eslint/markdown": "^7.5.1",
|
|
14
|
+
"@types/node": "^22.15.0",
|
|
15
|
+
"eslint": "^9.39.1",
|
|
16
|
+
"eslint-plugin-html": "^7.1.0",
|
|
17
|
+
"globals": "^15.11.0",
|
|
18
|
+
"prettier": "^3.7.1",
|
|
19
|
+
"tsup": "^8.4.0",
|
|
20
|
+
"tsx": "^4.19.0",
|
|
21
|
+
"typescript": "^5.8.0",
|
|
22
|
+
"typescript-eslint": "^8.31.0",
|
|
23
|
+
"vitest": "^3.1.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"name": "@jrc03c/gt-cli",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup && chmod +x dist/index.js",
|
|
34
|
+
"dev": "tsx src/index.ts",
|
|
35
|
+
"format": "prettier --write .",
|
|
36
|
+
"lint": "eslint .",
|
|
37
|
+
"pub": "npm version patch --force && npm publish --access=public",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest"
|
|
40
|
+
},
|
|
41
|
+
"type": "module",
|
|
42
|
+
"version": "0.1.1"
|
|
43
|
+
}
|