@uic-coe-connect/cli 0.2.2 → 0.3.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/dist/commands/deploy.js +15 -1
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.js +79 -0
- package/dist/commands/pipelines.js +7 -3
- package/dist/index.js +2 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -5,6 +5,12 @@ import { resolveApp } from "./apps.js";
|
|
|
5
5
|
/** Sentinels the deploy stream ends with — the server's own success signal. */
|
|
6
6
|
const OK = "__DEPLOY_OK__";
|
|
7
7
|
const FAILED = "__DEPLOY_FAILED__";
|
|
8
|
+
/**
|
|
9
|
+
* Machine markers the server interleaves for the web UI's pipeline diagram.
|
|
10
|
+
* They're meaningless here and read as debug noise, so they never reach the
|
|
11
|
+
* console or the JSON log — the stream already carries a human line for each.
|
|
12
|
+
*/
|
|
13
|
+
const MARKER = /^__(NODE|COMMIT)__:/;
|
|
8
14
|
export function registerDeployCommands() {
|
|
9
15
|
register({
|
|
10
16
|
name: "deploy",
|
|
@@ -49,11 +55,19 @@ export function registerDeployCommands() {
|
|
|
49
55
|
info(`Deploying ${app.id} · pipeline "${chosen.name}"${branch ? ` · branch ${branch}` : ""}`);
|
|
50
56
|
const lines = [];
|
|
51
57
|
const sentinels = [];
|
|
58
|
+
let commit;
|
|
52
59
|
await c.stream("POST", `/registered-apps/${app.id}/deploy`, body, (line) => {
|
|
53
60
|
if (line === OK || line === FAILED) {
|
|
54
61
|
sentinels.push(line);
|
|
55
62
|
return;
|
|
56
63
|
}
|
|
64
|
+
if (MARKER.test(line)) {
|
|
65
|
+
// Keep the one piece of information a marker carries that the
|
|
66
|
+
// human stream doesn't structure: which commit got deployed.
|
|
67
|
+
if (line.startsWith("__COMMIT__:"))
|
|
68
|
+
commit = line.split(":")[1];
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
57
71
|
lines.push(line);
|
|
58
72
|
// In JSON mode the log is part of the final payload instead, so stdout
|
|
59
73
|
// stays parseable; progress still shows on stderr.
|
|
@@ -70,7 +84,7 @@ export function registerDeployCommands() {
|
|
|
70
84
|
: sentinels.includes(FAILED)
|
|
71
85
|
? "failed"
|
|
72
86
|
: "unknown";
|
|
73
|
-
emit({ appId: app.id, pipeline: chosen.name, status: outcome, log: lines }, () => {
|
|
87
|
+
emit({ appId: app.id, pipeline: chosen.name, status: outcome, commit, log: lines }, () => {
|
|
74
88
|
info(outcome === "ok" ? `\n✓ Deploy succeeded` : `\n✗ Deploy ${outcome}`);
|
|
75
89
|
});
|
|
76
90
|
// A stream that ended without a sentinel means the connection dropped
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerLogCommands(): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CliError } from "../client.js";
|
|
2
|
+
import { emit, info } from "../output.js";
|
|
3
|
+
import { register } from "../registry.js";
|
|
4
|
+
import { resolveApp } from "./apps.js";
|
|
5
|
+
const FOLLOW_INTERVAL_MS = 2_000;
|
|
6
|
+
export function registerLogCommands() {
|
|
7
|
+
register({
|
|
8
|
+
name: "logs",
|
|
9
|
+
summary: "Read an app's pm2 logs (why it crashed, what it printed)",
|
|
10
|
+
usage: "logs <app> [--out] [--tail <n>] [--follow]",
|
|
11
|
+
details: [
|
|
12
|
+
"Defaults to the error log — that's where a startup crash lands.",
|
|
13
|
+
" --out stdout instead of stderr",
|
|
14
|
+
" --tail <n> how many lines (default 100, max 2000)",
|
|
15
|
+
" --follow keep polling and print new lines until Ctrl-C",
|
|
16
|
+
"",
|
|
17
|
+
"These are root's pm2 logs on the VM, which you otherwise can't read",
|
|
18
|
+
"without SSH. If a deploy just failed a health check it already printed",
|
|
19
|
+
"the tail inline — this is for looking further back.",
|
|
20
|
+
],
|
|
21
|
+
async run({ args, client }) {
|
|
22
|
+
const c = client();
|
|
23
|
+
const app = await resolveApp(c, args.arg(0, "app"));
|
|
24
|
+
const stream = args.bool("out") ? "out" : "err";
|
|
25
|
+
const tail = Number(args.flag("tail") ?? 100);
|
|
26
|
+
if (!Number.isFinite(tail) || tail < 1) {
|
|
27
|
+
throw new CliError(`--tail must be a positive number, got "${args.flag("tail")}".`, 6);
|
|
28
|
+
}
|
|
29
|
+
const path = `/registered-apps/${app.id}/logs?stream=${stream}&lines=${Math.min(tail, 2000)}`;
|
|
30
|
+
const fetchLogs = () => c.request("GET", path);
|
|
31
|
+
const first = await fetchLogs();
|
|
32
|
+
if (first.logs.length === 0) {
|
|
33
|
+
emit(first, () => {
|
|
34
|
+
info(first.note ?? `No pm2 process found for ${app.id}.`);
|
|
35
|
+
info(" Deploy it first, or check the pipeline's pm2 step names.");
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (args.bool("json")) {
|
|
40
|
+
emit(first, () => undefined);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const render = (block, lines) => {
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
info(first.logs.length > 1 ? `[${block.process}] ${line}` : line);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
for (const block of first.logs) {
|
|
49
|
+
info(`── ${block.process} (${block.status}, ${block.restarts} restarts) · ${stream === "out" ? "stdout" : "stderr"} ──`);
|
|
50
|
+
if (block.lines.length === 0) {
|
|
51
|
+
info(" (empty)");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
render(block, block.lines);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!args.bool("follow"))
|
|
58
|
+
return;
|
|
59
|
+
// Poll rather than hold a connection open: a deploy can restart the app
|
|
60
|
+
// (and rotate the file) underneath us, and re-reading the tail each time
|
|
61
|
+
// survives that where a held stream wouldn't.
|
|
62
|
+
const seen = new Map(first.logs.map((b) => [b.process, b.lines.at(-1)]));
|
|
63
|
+
info("── following (Ctrl-C to stop) ──");
|
|
64
|
+
for (;;) {
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, FOLLOW_INTERVAL_MS));
|
|
66
|
+
const next = await fetchLogs();
|
|
67
|
+
for (const block of next.logs) {
|
|
68
|
+
const last = seen.get(block.process);
|
|
69
|
+
const index = last === undefined ? -1 : block.lines.lastIndexOf(last);
|
|
70
|
+
const fresh = index >= 0 ? block.lines.slice(index + 1) : block.lines;
|
|
71
|
+
if (fresh.length > 0) {
|
|
72
|
+
render(block, fresh);
|
|
73
|
+
seen.set(block.process, fresh.at(-1));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -7,7 +7,7 @@ import { resolveApp } from "./apps.js";
|
|
|
7
7
|
* Steps are written as a compact `type:arg` list so a pipeline can be created
|
|
8
8
|
* in one command instead of hand-authoring JSON:
|
|
9
9
|
*
|
|
10
|
-
* pull scan
|
|
10
|
+
* pull scan env:backend npm:ci@backend migrate:backend chown pm2:restart@my-api
|
|
11
11
|
*
|
|
12
12
|
* `--steps-file` takes the raw JSON array instead, for anything this shorthand
|
|
13
13
|
* can't express.
|
|
@@ -22,6 +22,8 @@ function parseStep(token) {
|
|
|
22
22
|
return { type: "scan" };
|
|
23
23
|
case "chown":
|
|
24
24
|
return { type: "chown" };
|
|
25
|
+
case "env":
|
|
26
|
+
return { type: "env", subdir: arg || subdir || undefined };
|
|
25
27
|
case "npm":
|
|
26
28
|
if (!arg)
|
|
27
29
|
throw new CliError(`npm step needs a script: npm:ci or npm:build@subdir`, 6);
|
|
@@ -33,7 +35,7 @@ function parseStep(token) {
|
|
|
33
35
|
throw new CliError(`pm2 step needs an action: pm2:restart@process-name`, 6);
|
|
34
36
|
return { type: "pm2", action: arg, process: subdir || undefined };
|
|
35
37
|
default:
|
|
36
|
-
throw new CliError(`Unknown step "${token}".`, 6, "Valid: pull, scan, chown, npm:<script>[@subdir], migrate[:subdir], pm2:<action>[@process]");
|
|
38
|
+
throw new CliError(`Unknown step "${token}".`, 6, "Valid: pull, scan, env[:subdir], chown, npm:<script>[@subdir], migrate[:subdir], pm2:<action>[@process]");
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
function describeStep(step) {
|
|
@@ -42,6 +44,8 @@ function describeStep(step) {
|
|
|
42
44
|
return `npm ${step.script}${step.subdir ? ` (${step.subdir})` : ""}`;
|
|
43
45
|
case "migrate":
|
|
44
46
|
return `migrate${step.subdir ? ` (${step.subdir})` : ""}`;
|
|
47
|
+
case "env":
|
|
48
|
+
return `write .env${step.subdir ? ` (${step.subdir})` : ""}`;
|
|
45
49
|
case "pm2":
|
|
46
50
|
return `pm2 ${step.action}${step.process ? ` ${step.process}` : ""}`;
|
|
47
51
|
default:
|
|
@@ -114,7 +118,7 @@ export function registerPipelineCommands() {
|
|
|
114
118
|
summary: "Create a pipeline from a step list",
|
|
115
119
|
usage: 'pipelines create <app> --name "Full deploy" <step>... | --steps-file <f.json>',
|
|
116
120
|
details: [
|
|
117
|
-
"Steps: pull, scan, chown, npm:<script>[@subdir], migrate[:subdir], pm2:<action>[@process]",
|
|
121
|
+
"Steps: pull, scan, env[:subdir], chown, npm:<script>[@subdir], migrate[:subdir], pm2:<action>[@process]",
|
|
118
122
|
'Example: coe pipelines create myapp --name "Full" pull scan npm:ci@backend pm2:restart@myapp-api',
|
|
119
123
|
],
|
|
120
124
|
async run({ args, client }) {
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { registerAppCommands } from "./commands/apps.js";
|
|
|
5
5
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
6
6
|
import { registerDeployCommands } from "./commands/deploy.js";
|
|
7
7
|
import { registerEnvCommands } from "./commands/env.js";
|
|
8
|
+
import { registerLogCommands } from "./commands/logs.js";
|
|
8
9
|
import { registerPipelineCommands } from "./commands/pipelines.js";
|
|
9
10
|
import { registerResourceCommands } from "./commands/resources.js";
|
|
10
11
|
import { registerRoleCommands } from "./commands/roles.js";
|
|
@@ -18,6 +19,7 @@ registerAccessCommands();
|
|
|
18
19
|
registerEnvCommands();
|
|
19
20
|
registerResourceCommands();
|
|
20
21
|
registerRoleCommands();
|
|
22
|
+
registerLogCommands();
|
|
21
23
|
function parseArgs(argv) {
|
|
22
24
|
const positional = [];
|
|
23
25
|
const flags = {};
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uic-coe-connect/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "The coe CLI \u2014 manage COEConnect apps (pipelines, deploys, access, env) from a terminal, with browser-approved auth. Designed to be driven by an AI agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|