@agentuity/cli 2.0.12 → 2.0.14

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 (87) hide show
  1. package/dist/agent-detection.d.ts.map +1 -1
  2. package/dist/agent-detection.js +1 -0
  3. package/dist/agent-detection.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +15 -8
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  8. package/dist/cmd/cloud/sandbox/create.js +46 -4
  9. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  10. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  11. package/dist/cmd/cloud/sandbox/exec.js +4 -3
  12. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  13. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  14. package/dist/cmd/cloud/sandbox/run.js +9 -5
  15. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  16. package/dist/cmd/cloud/sandbox/snapshot/create.js +4 -4
  17. package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
  18. package/dist/cmd/coder/start.d.ts.map +1 -1
  19. package/dist/cmd/coder/start.js +1 -0
  20. package/dist/cmd/coder/start.js.map +1 -1
  21. package/dist/cmd/coder/workspace/common.d.ts +29 -0
  22. package/dist/cmd/coder/workspace/common.d.ts.map +1 -0
  23. package/dist/cmd/coder/workspace/common.js +83 -0
  24. package/dist/cmd/coder/workspace/common.js.map +1 -0
  25. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  26. package/dist/cmd/coder/workspace/create.js +34 -37
  27. package/dist/cmd/coder/workspace/create.js.map +1 -1
  28. package/dist/cmd/coder/workspace/get.d.ts.map +1 -1
  29. package/dist/cmd/coder/workspace/get.js +2 -5
  30. package/dist/cmd/coder/workspace/get.js.map +1 -1
  31. package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
  32. package/dist/cmd/coder/workspace/index.js +10 -0
  33. package/dist/cmd/coder/workspace/index.js.map +1 -1
  34. package/dist/cmd/coder/workspace/list.d.ts.map +1 -1
  35. package/dist/cmd/coder/workspace/list.js +4 -0
  36. package/dist/cmd/coder/workspace/list.js.map +1 -1
  37. package/dist/cmd/coder/workspace/refresh.d.ts +2 -0
  38. package/dist/cmd/coder/workspace/refresh.d.ts.map +1 -0
  39. package/dist/cmd/coder/workspace/refresh.js +59 -0
  40. package/dist/cmd/coder/workspace/refresh.js.map +1 -0
  41. package/dist/cmd/coder/workspace/update.d.ts +2 -0
  42. package/dist/cmd/coder/workspace/update.d.ts.map +1 -0
  43. package/dist/cmd/coder/workspace/update.js +131 -0
  44. package/dist/cmd/coder/workspace/update.js.map +1 -0
  45. package/dist/cmd/coder/workspace/validate-dependencies.d.ts +2 -0
  46. package/dist/cmd/coder/workspace/validate-dependencies.d.ts.map +1 -0
  47. package/dist/cmd/coder/workspace/validate-dependencies.js +70 -0
  48. package/dist/cmd/coder/workspace/validate-dependencies.js.map +1 -0
  49. package/dist/cmd/project/random-name.d.ts +17 -0
  50. package/dist/cmd/project/random-name.d.ts.map +1 -0
  51. package/dist/cmd/project/random-name.js +144 -0
  52. package/dist/cmd/project/random-name.js.map +1 -0
  53. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  54. package/dist/cmd/project/template-flow.js +181 -153
  55. package/dist/cmd/project/template-flow.js.map +1 -1
  56. package/dist/composite-logger.d.ts.map +1 -1
  57. package/dist/composite-logger.js +19 -0
  58. package/dist/composite-logger.js.map +1 -1
  59. package/dist/config.d.ts +18 -16
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +46 -16
  62. package/dist/config.js.map +1 -1
  63. package/dist/tui/prompt.d.ts +29 -0
  64. package/dist/tui/prompt.d.ts.map +1 -1
  65. package/dist/tui/prompt.js +180 -8
  66. package/dist/tui/prompt.js.map +1 -1
  67. package/package.json +7 -7
  68. package/src/agent-detection.ts +1 -0
  69. package/src/cli.ts +30 -8
  70. package/src/cmd/cloud/sandbox/create.ts +57 -4
  71. package/src/cmd/cloud/sandbox/exec.ts +4 -3
  72. package/src/cmd/cloud/sandbox/run.ts +9 -5
  73. package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
  74. package/src/cmd/coder/start.ts +1 -0
  75. package/src/cmd/coder/workspace/common.ts +103 -0
  76. package/src/cmd/coder/workspace/create.ts +39 -43
  77. package/src/cmd/coder/workspace/get.ts +2 -5
  78. package/src/cmd/coder/workspace/index.ts +10 -0
  79. package/src/cmd/coder/workspace/list.ts +4 -0
  80. package/src/cmd/coder/workspace/refresh.ts +63 -0
  81. package/src/cmd/coder/workspace/update.ts +154 -0
  82. package/src/cmd/coder/workspace/validate-dependencies.ts +75 -0
  83. package/src/cmd/project/random-name.ts +152 -0
  84. package/src/cmd/project/template-flow.ts +199 -161
  85. package/src/composite-logger.ts +20 -0
  86. package/src/config.ts +69 -19
  87. package/src/tui/prompt.ts +214 -8
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync } from 'node:fs';
2
- import { chmod, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
- import { basename, extname, join, normalize, resolve } from 'node:path';
4
+ import { basename, dirname, extname, join, normalize, resolve } from 'node:path';
5
5
  import { type Logger, StructuredError } from '@agentuity/core';
6
6
  import {
7
7
  type BuildMetadata,
@@ -15,6 +15,7 @@ import { z } from 'zod';
15
15
  import { clearProfileCache } from './cache';
16
16
  import { getCatalystUrl } from './catalyst';
17
17
  import { readEnvFile, writeEnvFile } from './env-util';
18
+ import { ErrorCode } from './errors';
18
19
  import {
19
20
  deleteAuthFromKeychain,
20
21
  getAuthFromKeychain,
@@ -151,10 +152,12 @@ function expandTilde(path: string): string {
151
152
  let cachedConfig: Config | null | undefined;
152
153
  // Track the resolved config path so saveConfig writes back to the same file
153
154
  let cachedConfigPath: string | undefined;
155
+ const loadedProjectConfigPaths = new Map<string, string>();
154
156
 
155
157
  export function resetConfigCache(): void {
156
158
  cachedConfig = undefined;
157
159
  cachedConfigPath = undefined;
160
+ loadedProjectConfigPaths.clear();
158
161
  }
159
162
 
160
163
  export async function loadConfig(
@@ -600,31 +603,76 @@ export function generateYAMLTemplate(name: string): string {
600
603
  return lines.join('\n');
601
604
  }
602
605
 
603
- export const ProjectConfigNotFoundException = StructuredError('ProjectConfigNotFoundException');
606
+ export const ProjectConfigNotFoundException = StructuredError('ProjectConfigNotFoundException')<{
607
+ code?: ErrorCode;
608
+ configPath?: string;
609
+ explicit?: boolean;
610
+ }>();
604
611
 
605
612
  type ProjectConfig = z.infer<typeof ProjectSchema>;
606
613
 
607
- export async function loadProjectConfig(
608
- dir: string,
614
+ export type ResolvedProjectConfigPaths = {
615
+ projectDir: string;
616
+ configPath: string;
617
+ explicitConfigFile: boolean;
618
+ };
619
+
620
+ export async function resolveProjectConfigPaths(
621
+ path: string,
609
622
  config?: Config | null
610
- ): Promise<ProjectConfig> {
611
- let configPath = join(dir, 'agentuity.json');
623
+ ): Promise<ResolvedProjectConfigPaths> {
624
+ const resolvedPath = resolve(expandTilde(path));
625
+ const pathStats = await stat(resolvedPath).catch(() => null);
626
+ const isExplicitJsonFile =
627
+ (pathStats?.isFile() || pathStats === null) && extname(resolvedPath) === '.json';
628
+
629
+ if (isExplicitJsonFile) {
630
+ return {
631
+ projectDir: dirname(resolvedPath),
632
+ configPath: resolvedPath,
633
+ explicitConfigFile: true,
634
+ };
635
+ }
636
+
637
+ const projectDir = resolvedPath;
638
+ let configPath = join(projectDir, 'agentuity.json');
612
639
 
613
- // Check for profile-specific override if config is provided
614
640
  if (config?.name) {
615
- const profileConfigPath = join(dir, `agentuity.${config.name}.json`);
641
+ const profileConfigPath = join(projectDir, `agentuity.${config.name}.json`);
616
642
  if (await Bun.file(profileConfigPath).exists()) {
617
643
  configPath = profileConfigPath;
618
644
  }
619
645
  }
620
646
 
647
+ return {
648
+ projectDir,
649
+ configPath,
650
+ explicitConfigFile: false,
651
+ };
652
+ }
653
+
654
+ export async function loadProjectConfig(
655
+ dir: string,
656
+ config?: Config | null
657
+ ): Promise<ProjectConfig> {
658
+ const { projectDir, configPath, explicitConfigFile } = await resolveProjectConfigPaths(
659
+ dir,
660
+ config
661
+ );
662
+
621
663
  const file = Bun.file(configPath);
622
664
  if (!(await file.exists())) {
623
665
  // TODO: check to see if a valid project that was created unauthenticated
624
666
  // and then if so:
625
667
  // 1. if authentication, offer to import the project
626
668
  // 2. tell them that they need to login to use the command and import the project
627
- throw new ProjectConfigNotFoundException({ message: 'project config not found' });
669
+ throw new ProjectConfigNotFoundException({
670
+ message: explicitConfigFile
671
+ ? `Project config not found at ${configPath}`
672
+ : 'project config not found',
673
+ configPath,
674
+ explicit: explicitConfigFile,
675
+ });
628
676
  }
629
677
  const text = await file.text();
630
678
  const parsedConfig = parseJSONC(text);
@@ -637,6 +685,7 @@ export async function loadProjectConfig(
637
685
  }
638
686
  process.exit(1);
639
687
  }
688
+ loadedProjectConfigPaths.set(projectDir, configPath);
640
689
  return result.data;
641
690
  }
642
691
 
@@ -717,18 +766,19 @@ export async function updateProjectConfig(
717
766
  updates: Partial<z.infer<typeof ProjectSchema>>,
718
767
  config?: Config | null
719
768
  ): Promise<void> {
720
- let configPath = join(dir, 'agentuity.json');
721
-
722
- if (config?.name) {
723
- const profileConfigPath = join(dir, `agentuity.${config.name}.json`);
724
- if (await Bun.file(profileConfigPath).exists()) {
725
- configPath = profileConfigPath;
726
- }
727
- }
769
+ const resolved = await resolveProjectConfigPaths(dir, config);
770
+ const configPath = resolved.explicitConfigFile
771
+ ? resolved.configPath
772
+ : (loadedProjectConfigPaths.get(resolved.projectDir) ?? resolved.configPath);
728
773
 
729
774
  const file = Bun.file(configPath);
730
775
  if (!(await file.exists())) {
731
- throw new Error(`Project config not found at ${configPath}`);
776
+ throw new ProjectConfigNotFoundException({
777
+ code: ErrorCode.PROJECT_NOT_FOUND,
778
+ message: `Project config not found at ${configPath}`,
779
+ configPath: resolved.configPath,
780
+ explicit: resolved.explicitConfigFile,
781
+ });
732
782
  }
733
783
 
734
784
  const text = await file.text();
package/src/tui/prompt.ts CHANGED
@@ -14,6 +14,12 @@ export interface TextOptions {
14
14
  message: string;
15
15
  initial?: string;
16
16
  hint?: string;
17
+ /**
18
+ * Pre-generated suggestion shown dim in the hint line.
19
+ * Submitting empty input accepts the placeholder.
20
+ * Unlike `initial`, this is rendered visibly so the user knows what they'll get.
21
+ */
22
+ placeholder?: string;
17
23
  validate?: (value: string) => boolean | string | Promise<boolean | string>;
18
24
  }
19
25
 
@@ -92,27 +98,39 @@ export class PromptFlow {
92
98
 
93
99
  /**
94
100
  * Text input prompt
101
+ *
102
+ * Two render paths:
103
+ * - With `placeholder`: custom raw-keypress renderer that paints the placeholder
104
+ * inline as dim ghost text at the cursor (autofill style). Vanishes on the first
105
+ * keystroke; Enter on an empty buffer accepts the placeholder.
106
+ * - Without `placeholder`: original readline-based renderer (untouched).
95
107
  */
96
108
  async text(options: TextOptions): Promise<string> {
97
- const { message, validate } = options;
98
- const initial = options.initial ?? '';
99
- const hasDefault = options.initial !== undefined;
109
+ const { message, validate, placeholder } = options;
110
+ // `placeholder` acts as a visible default: empty submit resolves to it.
111
+ // `initial` (legacy, invisible) still works but `placeholder` takes precedence.
112
+ const fallback = placeholder ?? options.initial ?? '';
113
+ const hasDefault = placeholder !== undefined || options.initial !== undefined;
100
114
 
101
115
  if (!this.isInteractive()) {
102
116
  if (hasDefault) {
103
- const validationResult = validate ? await validate(initial) : true;
117
+ const validationResult = validate ? await validate(fallback) : true;
104
118
  if (validationResult === true) {
105
- return initial;
119
+ return fallback;
106
120
  }
107
121
  // Validation failed - include the error message if it's a string
108
122
  const errorDetail = typeof validationResult === 'string' ? `: ${validationResult}` : '';
109
123
  throw this.nonInteractiveError(
110
- `Cannot prompt for "${message}" in non-interactive mode. Validation failed for default value "${initial}"${errorDetail}.`
124
+ `Cannot prompt for "${message}" in non-interactive mode. Validation failed for default value "${fallback}"${errorDetail}.`
111
125
  );
112
126
  }
113
127
  throw this.nonInteractiveError(`Cannot prompt for "${message}" in non-interactive mode.`);
114
128
  }
115
129
 
130
+ if (placeholder) {
131
+ return this.textWithGhost(options, placeholder);
132
+ }
133
+
116
134
  return new Promise((resolve, reject) => {
117
135
  const rl = readline.createInterface({
118
136
  input: process.stdin,
@@ -139,8 +157,8 @@ export class PromptFlow {
139
157
 
140
158
  rl.on('line', async (input) => {
141
159
  const trimmed = input.trim();
142
- // After a validation error, require explicit input - don't fall back to initial
143
- const value = trimmed.length > 0 ? trimmed : hadValidationError ? '' : initial;
160
+ // After a validation error, require explicit input - don't fall back to default.
161
+ const value = trimmed.length > 0 ? trimmed : hadValidationError ? '' : fallback;
144
162
 
145
163
  // Validate
146
164
  if (validate) {
@@ -229,6 +247,194 @@ export class PromptFlow {
229
247
  });
230
248
  }
231
249
 
250
+ /**
251
+ * Text input with inline ghost-text placeholder (autofill style).
252
+ *
253
+ * Layout:
254
+ * ◆ <message>
255
+ * │ <hint> (optional)
256
+ * │ <typed>│<ghost> ghost is dim, vanishes on first keystroke
257
+ *
258
+ * Behavior:
259
+ * - Typing any char hides the ghost permanently for this prompt instance.
260
+ * - Backspacing back to empty does NOT bring the ghost back.
261
+ * - Enter on empty buffer → resolves to placeholder.
262
+ * - Enter with typed text → resolves to typed text.
263
+ * - Validation error: shows inline error, ghost stays gone, user must type.
264
+ * - Ctrl+C: cancels.
265
+ */
266
+ private async textWithGhost(options: TextOptions, placeholder: string): Promise<string> {
267
+ const { message, validate, hint } = options;
268
+
269
+ return new Promise((resolve, reject) => {
270
+ let buffer = '';
271
+ // Tracks whether the ghost should still be visible. Once any printable key is
272
+ // pressed it goes false and stays false for the rest of this prompt.
273
+ let ghostVisible = true;
274
+ let hasError = false;
275
+ let errorMsg = '';
276
+
277
+ const inputPrefix = `${colors.secondary(symbols.bar)} `;
278
+ // Visible-character length of the input-line prefix ("│ " = 3 cells).
279
+ const PREFIX_VISIBLE = 3;
280
+
281
+ /**
282
+ * Repaint everything from the message line down. Cursor must be on the
283
+ * message line (column 0) when this is called for the first time, or we
284
+ * just moved up to it after a clear.
285
+ */
286
+ const paint = () => {
287
+ const symbol = hasError ? colors.error(symbols.error) : colors.active(symbols.active);
288
+ process.stdout.write(`${symbol} ${message}\n`);
289
+
290
+ if (hasError) {
291
+ process.stdout.write(
292
+ `${colors.secondary(symbols.bar)} ${colors.error(errorMsg)}\n`
293
+ );
294
+ } else if (hint) {
295
+ process.stdout.write(`${colors.secondary(symbols.bar)} ${colors.muted(hint)}\n`);
296
+ }
297
+
298
+ // Input line.
299
+ process.stdout.write(inputPrefix);
300
+ process.stdout.write(buffer);
301
+
302
+ if (ghostVisible && buffer.length === 0) {
303
+ // Paint the ghost, then move the cursor back to the start of it so the
304
+ // caret sits where the user would start typing.
305
+ process.stdout.write(colors.muted(placeholder));
306
+ readline.moveCursor(process.stdout, -placeholder.length, 0);
307
+ }
308
+ };
309
+
310
+ /**
311
+ * Number of terminal lines currently occupied by our render, so we know
312
+ * how many to clear on the next repaint.
313
+ * Always: message (1) + hint-or-error (0/1) + input (1).
314
+ */
315
+ const renderedLines = (): number => {
316
+ let n = 1; // message
317
+ if (hasError || hint) n += 1;
318
+ n += 1; // input
319
+ return n;
320
+ };
321
+
322
+ const repaint = () => {
323
+ // Move cursor to column 0, then up to the start of our render block, then clear.
324
+ readline.cursorTo(process.stdout, 0);
325
+ readline.moveCursor(process.stdout, 0, -(renderedLines() - 1));
326
+ readline.clearScreenDown(process.stdout);
327
+ paint();
328
+ };
329
+
330
+ // Resume stdin if it was paused by a prior prompt.
331
+ if (process.stdin.isTTY && process.stdin.isPaused()) {
332
+ process.stdin.resume();
333
+ }
334
+
335
+ readline.emitKeypressEvents(process.stdin);
336
+ if (process.stdin.isTTY) {
337
+ process.stdin.setRawMode(true);
338
+ }
339
+
340
+ // Initial paint.
341
+ paint();
342
+
343
+ const cleanup = () => {
344
+ process.stdin.removeListener('keypress', onKeypress);
345
+ if (process.stdin.isTTY) {
346
+ process.stdin.setRawMode(false);
347
+ process.stdin.pause();
348
+ }
349
+ };
350
+
351
+ const finalize = (value: string) => {
352
+ // Repaint as completed: replace whole render block with the completed lines.
353
+ readline.cursorTo(process.stdout, 0);
354
+ readline.moveCursor(process.stdout, 0, -(renderedLines() - 1));
355
+ readline.clearScreenDown(process.stdout);
356
+
357
+ if (value === '') {
358
+ process.stdout.write(
359
+ `${colors.completed(symbols.completed)} ${message}\n${colors.secondary(symbols.bar)}\n`
360
+ );
361
+ } else {
362
+ process.stdout.write(
363
+ `${colors.completed(symbols.completed)} ${message}\n${colors.secondary(symbols.bar)} ${colors.muted(value)}\n${colors.secondary(symbols.bar)}\n`
364
+ );
365
+ }
366
+
367
+ this.states.push({ type: 'completed', message, value });
368
+ cleanup();
369
+ resolve(value);
370
+ };
371
+
372
+ const onKeypress = async (str: string, key: KeypressEvent) => {
373
+ if (key.ctrl && key.name === 'c') {
374
+ cleanup();
375
+ console.log('\n');
376
+ this.cancel('Operation cancelled');
377
+ reject(new Error('User cancelled'));
378
+ return;
379
+ }
380
+
381
+ if (key.name === 'return') {
382
+ const trimmed = buffer.trim();
383
+ // Empty submit accepts the placeholder, but only if no error has occurred
384
+ // (after an error the user must type explicitly — same rule the readline
385
+ // path uses).
386
+ const value = trimmed.length > 0 ? trimmed : hasError ? '' : placeholder;
387
+
388
+ if (validate) {
389
+ try {
390
+ const result = await validate(value);
391
+ if (result !== true) {
392
+ errorMsg = typeof result === 'string' ? result : 'Invalid input';
393
+ hasError = true;
394
+ ghostVisible = false;
395
+ repaint();
396
+ return;
397
+ }
398
+ } catch (err) {
399
+ errorMsg = err instanceof Error ? err.message : 'Validation failed';
400
+ hasError = true;
401
+ ghostVisible = false;
402
+ repaint();
403
+ return;
404
+ }
405
+ }
406
+
407
+ finalize(value);
408
+ return;
409
+ }
410
+
411
+ if (key.name === 'backspace') {
412
+ if (buffer.length > 0) {
413
+ buffer = buffer.slice(0, -1);
414
+ repaint();
415
+ }
416
+ return;
417
+ }
418
+
419
+ // Printable single character (ignore arrow keys, function keys, etc.).
420
+ if (str && str.length === 1 && !key.ctrl && str >= ' ' && str !== '\x7f') {
421
+ buffer += str;
422
+ ghostVisible = false;
423
+ repaint();
424
+ return;
425
+ }
426
+
427
+ // Everything else (arrows, tab, etc.) is ignored.
428
+ };
429
+
430
+ // Mark the prefix length as used so the unused-var rule doesn't trip when we
431
+ // extend this in future. (Keeping it documented for cursor-math sanity.)
432
+ void PREFIX_VISIBLE;
433
+
434
+ process.stdin.on('keypress', onKeypress);
435
+ });
436
+ }
437
+
232
438
  /**
233
439
  * Confirm (yes/no) prompt
234
440
  */