@agentuity/cli 1.0.40 → 1.0.42
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/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +3 -3
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/typecheck.d.ts.map +1 -1
- package/dist/cmd/build/typecheck.js +52 -1
- package/dist/cmd/build/typecheck.js.map +1 -1
- package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
- package/dist/cmd/build/vite/static-renderer.js +22 -8
- package/dist/cmd/build/vite/static-renderer.js.map +1 -1
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/index.js +4 -0
- package/dist/cmd/cloud/index.js.map +1 -1
- package/dist/cmd/cloud/monitor.d.ts +3 -0
- package/dist/cmd/cloud/monitor.d.ts.map +1 -0
- package/dist/cmd/cloud/monitor.js +300 -0
- package/dist/cmd/cloud/monitor.js.map +1 -0
- package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
- package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/activity.js +54 -0
- package/dist/cmd/cloud/oidc/activity.js.map +1 -0
- package/dist/cmd/cloud/oidc/create.d.ts +2 -0
- package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/create.js +201 -0
- package/dist/cmd/cloud/oidc/create.js.map +1 -0
- package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
- package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/delete.js +56 -0
- package/dist/cmd/cloud/oidc/delete.js.map +1 -0
- package/dist/cmd/cloud/oidc/get.d.ts +2 -0
- package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/get.js +59 -0
- package/dist/cmd/cloud/oidc/get.js.map +1 -0
- package/dist/cmd/cloud/oidc/index.d.ts +3 -0
- package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/index.js +32 -0
- package/dist/cmd/cloud/oidc/index.js.map +1 -0
- package/dist/cmd/cloud/oidc/list.d.ts +2 -0
- package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/list.js +45 -0
- package/dist/cmd/cloud/oidc/list.js.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js +63 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
- package/dist/cmd/cloud/oidc/users.d.ts +2 -0
- package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/users.js +50 -0
- package/dist/cmd/cloud/oidc/users.js.map +1 -0
- package/dist/cmd/project/import.d.ts.map +1 -1
- package/dist/cmd/project/import.js +11 -1
- package/dist/cmd/project/import.js.map +1 -1
- package/dist/cmd/project/reconcile.d.ts +8 -0
- package/dist/cmd/project/reconcile.d.ts.map +1 -1
- package/dist/cmd/project/reconcile.js +150 -61
- package/dist/cmd/project/reconcile.js.map +1 -1
- package/dist/cmd/project/remote-import.d.ts +2 -0
- package/dist/cmd/project/remote-import.d.ts.map +1 -1
- package/dist/cmd/project/remote-import.js +2 -2
- package/dist/cmd/project/remote-import.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -6
- package/dist/config.js.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/schema-parser.js +6 -2
- package/dist/schema-parser.js.map +1 -1
- package/dist/utils/jsonc.d.ts +13 -0
- package/dist/utils/jsonc.d.ts.map +1 -0
- package/dist/utils/jsonc.js +63 -0
- package/dist/utils/jsonc.js.map +1 -0
- package/dist/utils/route-migration.d.ts +2 -1
- package/dist/utils/route-migration.d.ts.map +1 -1
- package/dist/utils/route-migration.js +23 -32
- package/dist/utils/route-migration.js.map +1 -1
- package/dist/utils/zip.d.ts.map +1 -1
- package/dist/utils/zip.js +18 -2
- package/dist/utils/zip.js.map +1 -1
- package/package.json +6 -7
- package/src/cli.ts +9 -1
- package/src/cmd/build/ast.ts +6 -3
- package/src/cmd/build/typecheck.ts +60 -1
- package/src/cmd/build/vite/static-renderer.ts +24 -8
- package/src/cmd/cloud/index.ts +4 -0
- package/src/cmd/cloud/monitor.ts +375 -0
- package/src/cmd/cloud/oidc/activity.ts +61 -0
- package/src/cmd/cloud/oidc/create.ts +232 -0
- package/src/cmd/cloud/oidc/delete.ts +63 -0
- package/src/cmd/cloud/oidc/get.ts +65 -0
- package/src/cmd/cloud/oidc/index.ts +35 -0
- package/src/cmd/cloud/oidc/list.ts +50 -0
- package/src/cmd/cloud/oidc/rotate-secret.ts +77 -0
- package/src/cmd/cloud/oidc/users.ts +57 -0
- package/src/cmd/project/import.ts +11 -1
- package/src/cmd/project/reconcile.ts +147 -61
- package/src/cmd/project/remote-import.ts +4 -2
- package/src/config.ts +24 -6
- package/src/schema-parser.ts +8 -2
- package/src/utils/jsonc.ts +67 -0
- package/src/utils/route-migration.ts +29 -40
- package/src/utils/zip.ts +17 -2
package/dist/utils/zip.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
1
2
|
import { relative } from 'node:path';
|
|
2
3
|
import { Glob } from 'bun';
|
|
3
4
|
import AdmZip from 'adm-zip';
|
|
4
5
|
import { toForwardSlash } from './normalize-path';
|
|
5
6
|
export async function zipDir(dir, outdir, options) {
|
|
6
7
|
const zip = new AdmZip();
|
|
7
|
-
const files = await Array.fromAsync(new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true }));
|
|
8
|
+
const files = await Array.fromAsync(new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true, followSymlinks: false }));
|
|
8
9
|
const total = files.length;
|
|
9
10
|
let count = 0;
|
|
10
11
|
for (const file of files) {
|
|
@@ -16,7 +17,22 @@ export async function zipDir(dir, outdir, options) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
if (!skip) {
|
|
19
|
-
|
|
20
|
+
try {
|
|
21
|
+
// Skip symlinks and directories — symlinks are workspace artefacts
|
|
22
|
+
// (e.g. bun's node_modules links) that cannot be resolved portably
|
|
23
|
+
// across machines and would cause EISDIR errors on extraction.
|
|
24
|
+
const stat = lstatSync(file);
|
|
25
|
+
if (!stat.isSymbolicLink() && !stat.isDirectory()) {
|
|
26
|
+
// Use addFile with explicit Unix permissions (0o644) instead of addLocalFile.
|
|
27
|
+
// On Windows, addLocalFile relies on OS file stats which may produce zip entries
|
|
28
|
+
// with incorrect Unix permission bits, causing EACCES errors when extracted on Linux.
|
|
29
|
+
const data = readFileSync(file);
|
|
30
|
+
zip.addFile(rel, data, '', 0o644);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
throw new Error(`Failed to add file to zip: ${rel} (${file})`, { cause: err });
|
|
35
|
+
}
|
|
20
36
|
}
|
|
21
37
|
count++;
|
|
22
38
|
if (options?.progress) {
|
package/dist/utils/zip.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zip.js","sourceRoot":"","sources":["../../src/utils/zip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOlD,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,GAAW,EAAE,MAAc,EAAE,OAAiB;IAC1E,MAAM,GAAG,GAAG,IAAI,MAAM,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,SAAS,CAClC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"zip.js","sourceRoot":"","sources":["../../src/utils/zip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOlD,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,GAAW,EAAE,MAAc,EAAE,OAAiB;IAC1E,MAAM,GAAG,GAAG,IAAI,MAAM,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,SAAS,CAClC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CACrF,CAAC;IACF,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,cAAc,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;gBAChC,IAAI,GAAG,IAAI,CAAC;YACb,CAAC;QACF,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;YACX,IAAI,CAAC;gBACJ,mEAAmE;gBACnE,mEAAmE;gBACnE,+DAA+D;gBAC/D,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;gBAC7B,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBACnD,8EAA8E;oBAC9E,iFAAiF;oBACjF,sFAAsF;oBACtF,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;oBAChC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;gBACnC,CAAC;YACF,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,KAAK,IAAI,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YAChF,CAAC;QACF,CAAC;QACD,KAAK,EAAE,CAAC;QACR,IAAI,OAAO,EAAE,QAAQ,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;YACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC3B,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,gDAAgD;QACtE,CAAC;IACF,CAAC;IACD,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,OAAO,EAAE,QAAQ,EAAE,CAAC;QACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACtB,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,gDAAgD;IACvE,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentuity/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.42",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Agentuity employees and contributors",
|
|
6
6
|
"type": "module",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"prepublishOnly": "bun run clean && bun run build"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@agentuity/auth": "1.0.
|
|
45
|
-
"@agentuity/core": "1.0.
|
|
46
|
-
"@agentuity/server": "1.0.
|
|
44
|
+
"@agentuity/auth": "1.0.42",
|
|
45
|
+
"@agentuity/core": "1.0.42",
|
|
46
|
+
"@agentuity/server": "1.0.42",
|
|
47
47
|
"@datasert/cronjs-parser": "^1.4.0",
|
|
48
48
|
"@vitejs/plugin-react": "^5.1.2",
|
|
49
49
|
"acorn-loose": "^8.5.2",
|
|
@@ -54,16 +54,15 @@
|
|
|
54
54
|
"enquirer": "^2.4.1",
|
|
55
55
|
"git-url-parse": "^16.1.0",
|
|
56
56
|
"json-colorizer": "^3.0.1",
|
|
57
|
-
"json5": "^2.2.3",
|
|
58
57
|
"tar": "^7.5.2",
|
|
59
58
|
"tar-fs": "^3.1.1",
|
|
60
59
|
"typescript": "^5.9.0",
|
|
61
60
|
"vite": "^7.2.7",
|
|
62
61
|
"zod": "^4.3.5",
|
|
63
|
-
"@agentuity/frontend": "1.0.
|
|
62
|
+
"@agentuity/frontend": "1.0.42"
|
|
64
63
|
},
|
|
65
64
|
"devDependencies": {
|
|
66
|
-
"@agentuity/test-utils": "1.0.
|
|
65
|
+
"@agentuity/test-utils": "1.0.42",
|
|
67
66
|
"@types/adm-zip": "^0.5.7",
|
|
68
67
|
"@types/bun": "latest",
|
|
69
68
|
"@types/tar-fs": "^2.0.4",
|
package/src/cli.ts
CHANGED
|
@@ -1215,7 +1215,7 @@ async function registerSubcommand(
|
|
|
1215
1215
|
}
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
-
// Add hidden --yes
|
|
1218
|
+
// Add hidden --yes and --force aliases for --confirm if command has a confirm option
|
|
1219
1219
|
if (subcommand.schema?.options) {
|
|
1220
1220
|
const parsed = parseOptionsSchema(subcommand.schema.options);
|
|
1221
1221
|
const hasConfirmOption = parsed.some((opt) => opt.name === 'confirm');
|
|
@@ -1224,6 +1224,14 @@ async function registerSubcommand(
|
|
|
1224
1224
|
const yesOption = cmd.createOption('--yes', 'Alias for --confirm');
|
|
1225
1225
|
yesOption.hideHelp();
|
|
1226
1226
|
cmd.addOption(yesOption);
|
|
1227
|
+
// Add hidden --force option that sets confirm to true,
|
|
1228
|
+
// but only if the schema doesn't already declare its own --force option
|
|
1229
|
+
const hasForceOption = parsed.some((opt) => opt.name === 'force');
|
|
1230
|
+
if (!hasForceOption) {
|
|
1231
|
+
const forceOption = cmd.createOption('--force', 'Alias for --confirm');
|
|
1232
|
+
forceOption.hideHelp();
|
|
1233
|
+
cmd.addOption(forceOption);
|
|
1234
|
+
}
|
|
1227
1235
|
}
|
|
1228
1236
|
}
|
|
1229
1237
|
|
package/src/cmd/build/ast.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { StructuredError, type WorkbenchConfig } from '@agentuity/core';
|
|
|
9
9
|
import type { LogLevel } from '../../types';
|
|
10
10
|
|
|
11
11
|
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
12
|
-
import
|
|
12
|
+
import { parseJSONC } from '../../utils/jsonc';
|
|
13
13
|
import { formatSchemaCode } from './format-schema';
|
|
14
14
|
import { toForwardSlash } from '../../utils/normalize-path';
|
|
15
15
|
import {
|
|
@@ -2963,8 +2963,11 @@ async function updateTsconfigPathMapping(rootDir: string, shouldAdd: boolean): P
|
|
|
2963
2963
|
try {
|
|
2964
2964
|
const tsconfigContent = await Bun.file(tsconfigPath).text();
|
|
2965
2965
|
|
|
2966
|
-
// Use
|
|
2967
|
-
const tsconfig =
|
|
2966
|
+
// Use JSONC parser to handle comments in tsconfig.json
|
|
2967
|
+
const tsconfig = parseJSONC(tsconfigContent) as {
|
|
2968
|
+
compilerOptions?: { paths?: Record<string, string[]> };
|
|
2969
|
+
[key: string]: unknown;
|
|
2970
|
+
};
|
|
2968
2971
|
const _before = JSON.stringify(tsconfig);
|
|
2969
2972
|
|
|
2970
2973
|
// Initialize compilerOptions and paths if they don't exist
|
|
@@ -24,6 +24,47 @@ export interface TypecheckOptions {
|
|
|
24
24
|
collector?: BuildReportCollector;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Filter tsc output to remove lines referencing node_modules paths.
|
|
29
|
+
*
|
|
30
|
+
* Some packages (e.g. @agentuity/runtime) ship .ts source files, which
|
|
31
|
+
* --skipLibCheck does not skip. Errors from those paths can crash the
|
|
32
|
+
* PEG-based tsc-output-parser because the parser expects every non-blank
|
|
33
|
+
* line to be a valid tsc error item. Stripping these lines before parsing
|
|
34
|
+
* prevents the crash and avoids surfacing errors the user cannot fix.
|
|
35
|
+
*
|
|
36
|
+
* We also strip continuation lines (indented with 2+ leading spaces) that
|
|
37
|
+
* follow a node_modules error line, as they are part of the same diagnostic.
|
|
38
|
+
*/
|
|
39
|
+
function filterNodeModulesErrors(output: string): string {
|
|
40
|
+
const lines = output.split('\n');
|
|
41
|
+
const filtered: string[] = [];
|
|
42
|
+
let skipping = false;
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
// A tsc error line starts with a path, e.g. "node_modules/..." or "../node_modules/..."
|
|
46
|
+
// Also handle Windows-style paths with backslashes
|
|
47
|
+
const isNodeModulesError =
|
|
48
|
+
/^\.{0,2}[/\\]?node_modules[/\\]/.test(line) ||
|
|
49
|
+
/^[A-Za-z]:[/\\].*node_modules[/\\]/.test(line);
|
|
50
|
+
|
|
51
|
+
if (isNodeModulesError) {
|
|
52
|
+
skipping = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Continuation lines of a multi-line tsc diagnostic start with whitespace
|
|
57
|
+
if (skipping && /^\s{2,}/.test(line)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
skipping = false;
|
|
62
|
+
filtered.push(line);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return filtered.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
/**
|
|
28
69
|
* run the typescript compiler and result formatted results
|
|
29
70
|
*
|
|
@@ -39,7 +80,25 @@ export async function typecheck(dir: string, options?: TypecheckOptions): Promis
|
|
|
39
80
|
.nothrow();
|
|
40
81
|
|
|
41
82
|
const output = await result.text();
|
|
42
|
-
|
|
83
|
+
|
|
84
|
+
// Filter out node_modules errors before parsing to prevent parser crashes.
|
|
85
|
+
// The PEG parser is strict and fails on lines it cannot match as tsc error items.
|
|
86
|
+
const filteredOutput = filterNodeModulesErrors(output);
|
|
87
|
+
|
|
88
|
+
let errors: GrammarItem[];
|
|
89
|
+
try {
|
|
90
|
+
errors = parse(filteredOutput) as GrammarItem[];
|
|
91
|
+
} catch {
|
|
92
|
+
// If the parser still fails (e.g. unexpected tsc output format), treat as
|
|
93
|
+
// an unknown error and show the raw output instead of crashing.
|
|
94
|
+
if (collector) {
|
|
95
|
+
collector.addGeneralError('typescript', output || result.stderr.toString());
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
output: output || result.stderr.toString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
43
102
|
|
|
44
103
|
if (result.exitCode === 0) {
|
|
45
104
|
return {
|
|
@@ -29,30 +29,46 @@ interface RouteTreeNode {
|
|
|
29
29
|
/**
|
|
30
30
|
* Walks a TanStack Router route tree and extracts all non-parameterized paths.
|
|
31
31
|
* Skips layout routes (no path) and parameterized routes (containing $).
|
|
32
|
+
*
|
|
33
|
+
* Accumulates the full URL path through the parent chain, since child routes
|
|
34
|
+
* under layout routes have relative paths (e.g., '/key-value' under a
|
|
35
|
+
* '/reference/api' layout should resolve to '/reference/api/key-value').
|
|
32
36
|
*/
|
|
33
37
|
function extractRoutePaths(node: RouteTreeNode): string[] {
|
|
34
38
|
const paths = new Set<string>();
|
|
35
39
|
|
|
36
|
-
function walk(route: RouteTreeNode) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
function walk(route: RouteTreeNode, parentPath: string) {
|
|
41
|
+
const segment: string | undefined = route.path ?? route.options?.path;
|
|
42
|
+
|
|
43
|
+
// Build the full path by accumulating segments from parent routes.
|
|
44
|
+
// - Layout routes have no path (undefined) and don't contribute to the URL.
|
|
45
|
+
// - Index routes have path '/' and resolve to the parent path itself.
|
|
46
|
+
// - Leaf/layout routes have paths like '/reference/api' or '/key-value'.
|
|
47
|
+
let currentPath = parentPath;
|
|
48
|
+
if (segment && segment !== '/') {
|
|
49
|
+
// Non-root segment: append to parent path.
|
|
50
|
+
// Segments always start with '/' (TanStack Router convention).
|
|
51
|
+
currentPath = parentPath === '/' ? segment : parentPath + segment;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add non-parameterized, non-empty paths
|
|
55
|
+
if (currentPath && !currentPath.includes('$')) {
|
|
56
|
+
const normalized = currentPath === '/' ? '/' : currentPath.replace(/\/+$/, '');
|
|
41
57
|
if (normalized) {
|
|
42
58
|
paths.add(normalized);
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
// Recurse into children
|
|
62
|
+
// Recurse into children, passing the accumulated path
|
|
47
63
|
const children = route.children;
|
|
48
64
|
if (children && typeof children === 'object') {
|
|
49
65
|
for (const child of Object.values(children)) {
|
|
50
|
-
if (child) walk(child);
|
|
66
|
+
if (child) walk(child, currentPath);
|
|
51
67
|
}
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
walk(node);
|
|
71
|
+
walk(node, '');
|
|
56
72
|
return [...paths].sort();
|
|
57
73
|
}
|
|
58
74
|
|
package/src/cmd/cloud/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import webhookCommand from './webhook';
|
|
|
14
14
|
import { agentCommand } from './agent';
|
|
15
15
|
import envCommand from './env';
|
|
16
16
|
import apikeyCommand from './apikey';
|
|
17
|
+
import oidcCommand from './oidc';
|
|
17
18
|
import streamCommand from './stream';
|
|
18
19
|
import vectorCommand from './vector';
|
|
19
20
|
import { emailCommand } from './email';
|
|
@@ -23,6 +24,7 @@ import scheduleCommand from './schedule';
|
|
|
23
24
|
import servicesCommand from './services';
|
|
24
25
|
import { regionSubcommand } from './region';
|
|
25
26
|
import { machineCommand } from './machine';
|
|
27
|
+
import { monitorSubcommand } from './monitor';
|
|
26
28
|
import { evalCommand } from './eval';
|
|
27
29
|
import { evalRunCommand } from './eval-run';
|
|
28
30
|
import { getCommand } from '../../command-prefix';
|
|
@@ -38,6 +40,7 @@ export const command = createCommand({
|
|
|
38
40
|
],
|
|
39
41
|
subcommands: [
|
|
40
42
|
apikeyCommand,
|
|
43
|
+
oidcCommand,
|
|
41
44
|
keyvalueCommand,
|
|
42
45
|
queueCommand,
|
|
43
46
|
webhookCommand,
|
|
@@ -60,6 +63,7 @@ export const command = createCommand({
|
|
|
60
63
|
threadCommand,
|
|
61
64
|
sshSubcommand,
|
|
62
65
|
scpSubcommand,
|
|
66
|
+
monitorSubcommand,
|
|
63
67
|
deploymentCommand,
|
|
64
68
|
regionSubcommand,
|
|
65
69
|
machineCommand,
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
getMonitorNode,
|
|
4
|
+
listDistressedNodes,
|
|
5
|
+
listMonitorNodeContainers,
|
|
6
|
+
listMonitorNodes,
|
|
7
|
+
MonitorWebSocketClient,
|
|
8
|
+
type MachineMonitorState,
|
|
9
|
+
type MonitorMessage,
|
|
10
|
+
type MonitorScope,
|
|
11
|
+
} from '@agentuity/core';
|
|
12
|
+
import { getAPIBaseURL } from '../../api';
|
|
13
|
+
import { getCommand } from '../../command-prefix';
|
|
14
|
+
import { createSubcommand } from '../../types';
|
|
15
|
+
import * as tui from '../../tui';
|
|
16
|
+
|
|
17
|
+
const monitorOptionsSchema = z.object({
|
|
18
|
+
machine: z.string().optional().describe('Monitor a specific machine id'),
|
|
19
|
+
deployment: z.string().optional().describe('Monitor machines for a deployment id'),
|
|
20
|
+
distressed: z.boolean().optional().describe('Only include distressed machines'),
|
|
21
|
+
snapshot: z.boolean().optional().describe('One-shot snapshot (no stream watch)'),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const monitorSubcommand = createSubcommand({
|
|
25
|
+
name: 'monitor',
|
|
26
|
+
description: 'Monitor infrastructure machines in real time',
|
|
27
|
+
tags: ['read-only', 'slow', 'requires-auth'],
|
|
28
|
+
requires: { auth: true, apiClient: true },
|
|
29
|
+
optional: { org: true },
|
|
30
|
+
idempotent: true,
|
|
31
|
+
examples: [
|
|
32
|
+
{ command: getCommand('cloud monitor --snapshot'), description: 'Show a monitor snapshot' },
|
|
33
|
+
{
|
|
34
|
+
command: getCommand('cloud monitor --distressed'),
|
|
35
|
+
description: 'Watch distressed machines',
|
|
36
|
+
},
|
|
37
|
+
{ command: getCommand('cloud monitor --machine mach_123'), description: 'Watch one machine' },
|
|
38
|
+
],
|
|
39
|
+
schema: {
|
|
40
|
+
options: monitorOptionsSchema,
|
|
41
|
+
},
|
|
42
|
+
webUrl: '/infrastructure/monitoring',
|
|
43
|
+
|
|
44
|
+
async handler(ctx) {
|
|
45
|
+
const { apiClient, options, opts, auth, config, orgId } = ctx;
|
|
46
|
+
|
|
47
|
+
if (opts.machine && opts.distressed) {
|
|
48
|
+
ctx.logger.fatal('--machine and --distressed are mutually exclusive.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (opts.deployment && opts.distressed) {
|
|
52
|
+
ctx.logger.fatal('--deployment and --distressed are mutually exclusive.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (opts.snapshot || options.json) {
|
|
56
|
+
const machines = await getSnapshotMachines({
|
|
57
|
+
apiClient,
|
|
58
|
+
machineId: opts.machine,
|
|
59
|
+
deploymentId: opts.deployment,
|
|
60
|
+
distressed: opts.distressed,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify(machines, null, 2));
|
|
65
|
+
return machines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
renderMachineTable(machines);
|
|
69
|
+
return machines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const machineMap = new Map<string, MachineMonitorState>();
|
|
73
|
+
|
|
74
|
+
const initialMachines = await getSnapshotMachines({
|
|
75
|
+
apiClient,
|
|
76
|
+
machineId: opts.machine,
|
|
77
|
+
deploymentId: opts.deployment,
|
|
78
|
+
distressed: opts.distressed,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const machine of initialMachines) {
|
|
82
|
+
machineMap.set(machine.machineId, machine);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
renderWatchTable(machineMap, {
|
|
86
|
+
mode: 'snapshot',
|
|
87
|
+
machineId: opts.machine,
|
|
88
|
+
deploymentId: opts.deployment,
|
|
89
|
+
distressed: opts.distressed,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
tui.info('Connecting to monitoring stream...');
|
|
93
|
+
|
|
94
|
+
let processingChain = Promise.resolve();
|
|
95
|
+
let resolveWait: (() => void) | null = null;
|
|
96
|
+
|
|
97
|
+
const monitorClient = new MonitorWebSocketClient({
|
|
98
|
+
baseUrl: getAPIBaseURL(config),
|
|
99
|
+
token: auth.apiKey,
|
|
100
|
+
orgId,
|
|
101
|
+
scope: toMonitorScope(opts.machine, opts.deployment),
|
|
102
|
+
onOpen: () => {
|
|
103
|
+
tui.success('Connected to monitoring stream');
|
|
104
|
+
},
|
|
105
|
+
onError: (error) => {
|
|
106
|
+
tui.error(`Monitoring stream error: ${error.message}`);
|
|
107
|
+
},
|
|
108
|
+
onClose: () => {
|
|
109
|
+
tui.info('Monitoring stream disconnected');
|
|
110
|
+
resolveWait?.();
|
|
111
|
+
},
|
|
112
|
+
onMessage: (message) => {
|
|
113
|
+
processingChain = processingChain.then(async () => {
|
|
114
|
+
await applyMonitorMessage(machineMap, message, apiClient, opts.deployment);
|
|
115
|
+
renderWatchTable(machineMap, {
|
|
116
|
+
mode: 'watch',
|
|
117
|
+
machineId: opts.machine,
|
|
118
|
+
deploymentId: opts.deployment,
|
|
119
|
+
distressed: opts.distressed,
|
|
120
|
+
lastMessage: message,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
monitorClient.connect();
|
|
127
|
+
|
|
128
|
+
await new Promise<void>((resolve) => {
|
|
129
|
+
resolveWait = resolve;
|
|
130
|
+
const onSigInt = () => {
|
|
131
|
+
monitorClient.close();
|
|
132
|
+
process.off('SIGINT', onSigInt);
|
|
133
|
+
resolve();
|
|
134
|
+
};
|
|
135
|
+
process.on('SIGINT', onSigInt);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return Array.from(machineMap.values());
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
async function getSnapshotMachines(params: {
|
|
143
|
+
apiClient: Parameters<typeof listMonitorNodes>[0];
|
|
144
|
+
machineId?: string;
|
|
145
|
+
deploymentId?: string;
|
|
146
|
+
distressed?: boolean;
|
|
147
|
+
}): Promise<MachineMonitorState[]> {
|
|
148
|
+
const { apiClient, machineId, deploymentId, distressed } = params;
|
|
149
|
+
|
|
150
|
+
let machines: MachineMonitorState[];
|
|
151
|
+
if (distressed) {
|
|
152
|
+
machines = await listDistressedNodes(apiClient);
|
|
153
|
+
} else if (machineId) {
|
|
154
|
+
machines = [await getMonitorNode(apiClient, machineId)];
|
|
155
|
+
} else {
|
|
156
|
+
machines = await listMonitorNodes(apiClient);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!deploymentId) {
|
|
160
|
+
return machines;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const results = await Promise.all(
|
|
164
|
+
machines.map(async (machine) => {
|
|
165
|
+
const containers = await listMonitorNodeContainers(apiClient, machine.machineId);
|
|
166
|
+
const hasDeployment = containers.some(
|
|
167
|
+
(container) => container.deploymentId === deploymentId
|
|
168
|
+
);
|
|
169
|
+
return hasDeployment ? machine : null;
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
return results.filter((m): m is MachineMonitorState => m !== null);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toMonitorScope(machineId?: string, deploymentId?: string): MonitorScope {
|
|
176
|
+
if (machineId) {
|
|
177
|
+
return { scope: 'machine', machineId };
|
|
178
|
+
}
|
|
179
|
+
if (deploymentId) {
|
|
180
|
+
return { scope: 'deployment', deploymentId };
|
|
181
|
+
}
|
|
182
|
+
return { scope: 'org' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function applyMonitorMessage(
|
|
186
|
+
machineMap: Map<string, MachineMonitorState>,
|
|
187
|
+
message: MonitorMessage,
|
|
188
|
+
apiClient: Parameters<typeof listMonitorNodes>[0],
|
|
189
|
+
deploymentId?: string
|
|
190
|
+
) {
|
|
191
|
+
if (message.type === 'snapshot') {
|
|
192
|
+
machineMap.clear();
|
|
193
|
+
for (const machine of message.machines) {
|
|
194
|
+
machineMap.set(machine.machineId, machine);
|
|
195
|
+
}
|
|
196
|
+
if (deploymentId) {
|
|
197
|
+
await filterMapByDeployment(machineMap, apiClient, deploymentId);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (message.type === 'update') {
|
|
203
|
+
const existing = machineMap.get(message.machineId);
|
|
204
|
+
const next: MachineMonitorState = {
|
|
205
|
+
machineId: message.machineId,
|
|
206
|
+
orgId: existing?.orgId ?? '',
|
|
207
|
+
report: message.report,
|
|
208
|
+
compositeScore: message.report.capacity?.compositeScore ?? existing?.compositeScore ?? 0,
|
|
209
|
+
health: message.health,
|
|
210
|
+
reportedAt: usecToISO(message.report.reportedAtUs) ?? existing?.reportedAt ?? '',
|
|
211
|
+
updatedAt: new Date().toISOString(),
|
|
212
|
+
gravity: existing?.gravity ?? '',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (deploymentId) {
|
|
216
|
+
const containers = await listMonitorNodeContainers(apiClient, message.machineId);
|
|
217
|
+
const include = containers.some((container) => container.deploymentId === deploymentId);
|
|
218
|
+
if (!include) {
|
|
219
|
+
machineMap.delete(message.machineId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
machineMap.set(message.machineId, next);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const existing = machineMap.get(message.machineId);
|
|
229
|
+
if (existing) {
|
|
230
|
+
existing.health = message.health;
|
|
231
|
+
existing.updatedAt = new Date().toISOString();
|
|
232
|
+
machineMap.set(message.machineId, existing);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function filterMapByDeployment(
|
|
237
|
+
machineMap: Map<string, MachineMonitorState>,
|
|
238
|
+
apiClient: Parameters<typeof listMonitorNodes>[0],
|
|
239
|
+
deploymentId: string
|
|
240
|
+
) {
|
|
241
|
+
const entries = Array.from(machineMap.keys());
|
|
242
|
+
const results = await Promise.all(
|
|
243
|
+
entries.map(async (machineId) => {
|
|
244
|
+
const containers = await listMonitorNodeContainers(apiClient, machineId);
|
|
245
|
+
const include = containers.some((container) => container.deploymentId === deploymentId);
|
|
246
|
+
return { machineId, include };
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
for (const { machineId, include } of results) {
|
|
250
|
+
if (!include) {
|
|
251
|
+
machineMap.delete(machineId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderWatchTable(
|
|
257
|
+
machineMap: Map<string, MachineMonitorState>,
|
|
258
|
+
params: {
|
|
259
|
+
mode: 'snapshot' | 'watch';
|
|
260
|
+
machineId?: string;
|
|
261
|
+
deploymentId?: string;
|
|
262
|
+
distressed?: boolean;
|
|
263
|
+
lastMessage?: MonitorMessage;
|
|
264
|
+
}
|
|
265
|
+
) {
|
|
266
|
+
console.clear();
|
|
267
|
+
|
|
268
|
+
const subtitle = [
|
|
269
|
+
params.mode === 'watch' ? 'Live mode' : 'Snapshot mode',
|
|
270
|
+
params.machineId ? `machine=${params.machineId}` : undefined,
|
|
271
|
+
params.deploymentId ? `deployment=${params.deploymentId}` : undefined,
|
|
272
|
+
params.distressed ? 'distressed=true' : undefined,
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(' • ');
|
|
276
|
+
|
|
277
|
+
tui.header('Cloud Monitor');
|
|
278
|
+
if (subtitle) {
|
|
279
|
+
tui.info(subtitle);
|
|
280
|
+
}
|
|
281
|
+
if (params.lastMessage && params.lastMessage.type === 'state_change') {
|
|
282
|
+
tui.warning(
|
|
283
|
+
`${params.lastMessage.machineId}: ${params.lastMessage.previousHealth} -> ${params.lastMessage.health}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
renderMachineTable(Array.from(machineMap.values()));
|
|
288
|
+
tui.info('Press Ctrl+C to stop watching.');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderMachineTable(machines: MachineMonitorState[]) {
|
|
292
|
+
if (machines.length === 0) {
|
|
293
|
+
tui.info('No machines found');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const rows = machines
|
|
298
|
+
.slice()
|
|
299
|
+
.sort((a, b) => a.machineId.localeCompare(b.machineId))
|
|
300
|
+
.map((machine) => ({
|
|
301
|
+
Machine: machine.machineId,
|
|
302
|
+
Health: formatHealth(machine.health),
|
|
303
|
+
CPU: formatPercent(machine.report?.host?.cpu?.usagePercent),
|
|
304
|
+
Memory: formatPercent(machine.report?.host?.memory?.usagePercent),
|
|
305
|
+
Disk: formatPercent(maxDiskUsage(machine.report?.host?.disks)),
|
|
306
|
+
Pressure: formatScore(machine.compositeScore),
|
|
307
|
+
Containers: `${machine.report?.capacity?.runningContainers ?? 0}/${machine.report?.capacity?.totalContainers ?? 0}`,
|
|
308
|
+
'Last Report': formatAge(machine.reportedAt),
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
tui.table(rows, [
|
|
312
|
+
{ name: 'Machine' },
|
|
313
|
+
{ name: 'Health' },
|
|
314
|
+
{ name: 'CPU', alignment: 'right' },
|
|
315
|
+
{ name: 'Memory', alignment: 'right' },
|
|
316
|
+
{ name: 'Disk', alignment: 'right' },
|
|
317
|
+
{ name: 'Pressure', alignment: 'right' },
|
|
318
|
+
{ name: 'Containers', alignment: 'right' },
|
|
319
|
+
{ name: 'Last Report' },
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function formatHealth(health: string): string {
|
|
324
|
+
if (health === 'CONNECTED') return '● CONNECTED';
|
|
325
|
+
if (health === 'STALE') return '◌ STALE';
|
|
326
|
+
if (health === 'DISCONNECTED') return '○ DISCONNECTED';
|
|
327
|
+
return health;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatPercent(value?: number): string {
|
|
331
|
+
if (value === undefined || value === null || Number.isNaN(value)) {
|
|
332
|
+
return '-';
|
|
333
|
+
}
|
|
334
|
+
return `${value.toFixed(1)}%`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatScore(score?: number): string {
|
|
338
|
+
if (score === undefined || score === null || Number.isNaN(score)) {
|
|
339
|
+
return '-';
|
|
340
|
+
}
|
|
341
|
+
if (score >= 0.85) {
|
|
342
|
+
return `${score.toFixed(2)} ⚠`;
|
|
343
|
+
}
|
|
344
|
+
return score.toFixed(2);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function maxDiskUsage(disks?: Array<{ usagePercent: number }>): number | undefined {
|
|
348
|
+
if (!disks || disks.length === 0) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
return Math.max(...disks.map((d) => d.usagePercent));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatAge(timestamp: string): string {
|
|
355
|
+
const date = new Date(timestamp);
|
|
356
|
+
const time = date.getTime();
|
|
357
|
+
if (Number.isNaN(time)) {
|
|
358
|
+
return '-';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const diff = Date.now() - time;
|
|
362
|
+
if (diff < 60_000) return `${Math.max(0, Math.floor(diff / 1000))}s ago`;
|
|
363
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
364
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
365
|
+
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function usecToISO(us?: number): string | undefined {
|
|
369
|
+
if (us === undefined || us <= 0 || Number.isNaN(us)) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return new Date(Math.floor(us / 1000)).toISOString();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default monitorSubcommand;
|