@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.
Files changed (102) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +9 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cmd/build/ast.d.ts.map +1 -1
  5. package/dist/cmd/build/ast.js +3 -3
  6. package/dist/cmd/build/ast.js.map +1 -1
  7. package/dist/cmd/build/typecheck.d.ts.map +1 -1
  8. package/dist/cmd/build/typecheck.js +52 -1
  9. package/dist/cmd/build/typecheck.js.map +1 -1
  10. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  11. package/dist/cmd/build/vite/static-renderer.js +22 -8
  12. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  13. package/dist/cmd/cloud/index.d.ts.map +1 -1
  14. package/dist/cmd/cloud/index.js +4 -0
  15. package/dist/cmd/cloud/index.js.map +1 -1
  16. package/dist/cmd/cloud/monitor.d.ts +3 -0
  17. package/dist/cmd/cloud/monitor.d.ts.map +1 -0
  18. package/dist/cmd/cloud/monitor.js +300 -0
  19. package/dist/cmd/cloud/monitor.js.map +1 -0
  20. package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
  21. package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
  22. package/dist/cmd/cloud/oidc/activity.js +54 -0
  23. package/dist/cmd/cloud/oidc/activity.js.map +1 -0
  24. package/dist/cmd/cloud/oidc/create.d.ts +2 -0
  25. package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
  26. package/dist/cmd/cloud/oidc/create.js +201 -0
  27. package/dist/cmd/cloud/oidc/create.js.map +1 -0
  28. package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
  29. package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
  30. package/dist/cmd/cloud/oidc/delete.js +56 -0
  31. package/dist/cmd/cloud/oidc/delete.js.map +1 -0
  32. package/dist/cmd/cloud/oidc/get.d.ts +2 -0
  33. package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
  34. package/dist/cmd/cloud/oidc/get.js +59 -0
  35. package/dist/cmd/cloud/oidc/get.js.map +1 -0
  36. package/dist/cmd/cloud/oidc/index.d.ts +3 -0
  37. package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
  38. package/dist/cmd/cloud/oidc/index.js +32 -0
  39. package/dist/cmd/cloud/oidc/index.js.map +1 -0
  40. package/dist/cmd/cloud/oidc/list.d.ts +2 -0
  41. package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
  42. package/dist/cmd/cloud/oidc/list.js +45 -0
  43. package/dist/cmd/cloud/oidc/list.js.map +1 -0
  44. package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
  45. package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
  46. package/dist/cmd/cloud/oidc/rotate-secret.js +63 -0
  47. package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
  48. package/dist/cmd/cloud/oidc/users.d.ts +2 -0
  49. package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
  50. package/dist/cmd/cloud/oidc/users.js +50 -0
  51. package/dist/cmd/cloud/oidc/users.js.map +1 -0
  52. package/dist/cmd/project/import.d.ts.map +1 -1
  53. package/dist/cmd/project/import.js +11 -1
  54. package/dist/cmd/project/import.js.map +1 -1
  55. package/dist/cmd/project/reconcile.d.ts +8 -0
  56. package/dist/cmd/project/reconcile.d.ts.map +1 -1
  57. package/dist/cmd/project/reconcile.js +150 -61
  58. package/dist/cmd/project/reconcile.js.map +1 -1
  59. package/dist/cmd/project/remote-import.d.ts +2 -0
  60. package/dist/cmd/project/remote-import.d.ts.map +1 -1
  61. package/dist/cmd/project/remote-import.js +2 -2
  62. package/dist/cmd/project/remote-import.js.map +1 -1
  63. package/dist/config.d.ts.map +1 -1
  64. package/dist/config.js +22 -6
  65. package/dist/config.js.map +1 -1
  66. package/dist/schema-parser.d.ts.map +1 -1
  67. package/dist/schema-parser.js +6 -2
  68. package/dist/schema-parser.js.map +1 -1
  69. package/dist/utils/jsonc.d.ts +13 -0
  70. package/dist/utils/jsonc.d.ts.map +1 -0
  71. package/dist/utils/jsonc.js +63 -0
  72. package/dist/utils/jsonc.js.map +1 -0
  73. package/dist/utils/route-migration.d.ts +2 -1
  74. package/dist/utils/route-migration.d.ts.map +1 -1
  75. package/dist/utils/route-migration.js +23 -32
  76. package/dist/utils/route-migration.js.map +1 -1
  77. package/dist/utils/zip.d.ts.map +1 -1
  78. package/dist/utils/zip.js +18 -2
  79. package/dist/utils/zip.js.map +1 -1
  80. package/package.json +6 -7
  81. package/src/cli.ts +9 -1
  82. package/src/cmd/build/ast.ts +6 -3
  83. package/src/cmd/build/typecheck.ts +60 -1
  84. package/src/cmd/build/vite/static-renderer.ts +24 -8
  85. package/src/cmd/cloud/index.ts +4 -0
  86. package/src/cmd/cloud/monitor.ts +375 -0
  87. package/src/cmd/cloud/oidc/activity.ts +61 -0
  88. package/src/cmd/cloud/oidc/create.ts +232 -0
  89. package/src/cmd/cloud/oidc/delete.ts +63 -0
  90. package/src/cmd/cloud/oidc/get.ts +65 -0
  91. package/src/cmd/cloud/oidc/index.ts +35 -0
  92. package/src/cmd/cloud/oidc/list.ts +50 -0
  93. package/src/cmd/cloud/oidc/rotate-secret.ts +77 -0
  94. package/src/cmd/cloud/oidc/users.ts +57 -0
  95. package/src/cmd/project/import.ts +11 -1
  96. package/src/cmd/project/reconcile.ts +147 -61
  97. package/src/cmd/project/remote-import.ts +4 -2
  98. package/src/config.ts +24 -6
  99. package/src/schema-parser.ts +8 -2
  100. package/src/utils/jsonc.ts +67 -0
  101. package/src/utils/route-migration.ts +29 -40
  102. 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
- zip.addLocalFile(file, undefined, rel);
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) {
@@ -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,CAC9D,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,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;QACxC,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"}
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.40",
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.40",
45
- "@agentuity/core": "1.0.40",
46
- "@agentuity/server": "1.0.40",
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.40"
62
+ "@agentuity/frontend": "1.0.42"
64
63
  },
65
64
  "devDependencies": {
66
- "@agentuity/test-utils": "1.0.40",
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 alias for --confirm if command has a confirm option
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
 
@@ -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 JSON5 from 'json5';
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 JSON5 to parse tsconfig.json (handles comments in input)
2967
- const tsconfig = JSON5.parse(tsconfigContent);
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
- const errors = parse(output) as GrammarItem[];
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 path: string | undefined = route.path ?? route.options?.path;
38
- if (path && !path.includes('$')) {
39
- // Normalize: strip trailing slashes, ensure leading slash
40
- const normalized = path === '/' ? '/' : path.replace(/\/+$/, '');
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 (TanStack Router stores them as an object)
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
 
@@ -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;