@outputai/cli 0.2.1-next.bd54540.0 → 0.2.1-next.eadab44.0

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.
@@ -162,6 +162,8 @@ export declare const WorkflowRunInfoStatus: {
162
162
  export interface WorkflowRunInfo {
163
163
  /** Unique identifier for this run */
164
164
  workflowId?: string;
165
+ /** The specific run id for this execution */
166
+ runId?: string;
165
167
  /** Name of the workflow definition */
166
168
  workflowType?: string;
167
169
  /** Current run status */
@@ -81,7 +81,7 @@ services:
81
81
  condition: service_healthy
82
82
  worker:
83
83
  condition: service_healthy
84
- image: outputai/api:${OUTPUT_API_VERSION:-0.2.1-next.bd54540.0}
84
+ image: outputai/api:${OUTPUT_API_VERSION:-0.2.1-next.eadab44.0}
85
85
  init: true
86
86
  networks:
87
87
  - main
@@ -7,6 +7,7 @@ export default class Init extends Command {
7
7
  };
8
8
  static flags: {
9
9
  'skip-env': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'skip-git': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  };
11
12
  run(): Promise<void>;
12
13
  }
@@ -18,12 +18,16 @@ export default class Init extends Command {
18
18
  'skip-env': Flags.boolean({
19
19
  description: 'Skip interactive environment variable configuration',
20
20
  default: false
21
+ }),
22
+ 'skip-git': Flags.boolean({
23
+ description: 'Skip git repository initialization',
24
+ default: false
21
25
  })
22
26
  };
23
27
  async run() {
24
28
  try {
25
29
  const { args, flags } = await this.parse(Init);
26
- await runInit(flags['skip-env'], args.folderName);
30
+ await runInit(flags['skip-env'], flags['skip-git'], args.folderName);
27
31
  }
28
32
  catch (error) {
29
33
  if (error instanceof UserCancelledError) {
@@ -37,7 +37,7 @@ describe('init command', () => {
37
37
  Object.defineProperty(cmd, 'parse', {
38
38
  value: vi.fn().mockResolvedValue({
39
39
  args: { folderName: undefined, ...parsedArgs },
40
- flags: { 'skip-env': false, ...flags }
40
+ flags: { 'skip-env': false, 'skip-git': false, ...flags }
41
41
  }),
42
42
  configurable: true
43
43
  });
@@ -52,10 +52,10 @@ describe('init command', () => {
52
52
  expect(cmd.run).toBeDefined();
53
53
  expect(typeof cmd.run).toBe('function');
54
54
  });
55
- it('should call runInit with skip-env flag and folder name', async () => {
55
+ it('should call runInit with skip-env flag, skip-git flag, and folder name', async () => {
56
56
  const cmd = createTestCommand();
57
57
  await cmd.run();
58
- expect(projectScaffold.runInit).toHaveBeenCalledWith(false, undefined);
58
+ expect(projectScaffold.runInit).toHaveBeenCalledWith(false, false, undefined);
59
59
  });
60
60
  it('should handle UserCancelledError', async () => {
61
61
  const error = new UserCancelledError();
@@ -89,12 +89,17 @@ describe('init command', () => {
89
89
  it('should pass skip-env flag to runInit when --skip-env is provided', async () => {
90
90
  const cmd = createTestCommand([], { 'skip-env': true });
91
91
  await cmd.run();
92
- expect(projectScaffold.runInit).toHaveBeenCalledWith(true, undefined);
92
+ expect(projectScaffold.runInit).toHaveBeenCalledWith(true, false, undefined);
93
+ });
94
+ it('should pass skip-git flag to runInit when --skip-git is provided', async () => {
95
+ const cmd = createTestCommand([], { 'skip-git': true });
96
+ await cmd.run();
97
+ expect(projectScaffold.runInit).toHaveBeenCalledWith(false, true, undefined);
93
98
  });
94
99
  it('should pass folder name to runInit when provided', async () => {
95
100
  const cmd = createTestCommand([], {}, { folderName: 'my-project' });
96
101
  await cmd.run();
97
- expect(projectScaffold.runInit).toHaveBeenCalledWith(false, 'my-project');
102
+ expect(projectScaffold.runInit).toHaveBeenCalledWith(false, false, 'my-project');
98
103
  });
99
104
  });
100
105
  });
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.2.1-next.bd54540.0"
2
+ "framework": "0.2.1-next.eadab44.0"
3
3
  }
@@ -1,3 +1,7 @@
1
1
  import { Hook } from '@oclif/core';
2
+ export declare const INTERACTIVE_FLAGS: string[];
3
+ export declare const GLOBAL_FLAGS: Set<string>;
4
+ export declare const hasInteractiveFlag: (argv: string[]) => boolean;
5
+ export declare const stripGlobalFlags: (argv: string[]) => void;
2
6
  declare const hook: Hook<'init'>;
3
7
  export default hook;
@@ -1,8 +1,20 @@
1
1
  import { ux } from '@oclif/core';
2
2
  import { checkForUpdate } from '#services/version_check.js';
3
3
  import { setNonInteractive } from '#utils/interactive.js';
4
- const hook = async function () {
5
- if (process.argv.includes('--yes') || process.argv.includes('--non-interactive')) {
4
+ export const INTERACTIVE_FLAGS = ['--yes', '--non-interactive'];
5
+ export const GLOBAL_FLAGS = new Set(INTERACTIVE_FLAGS);
6
+ export const hasInteractiveFlag = (argv) => argv.some(arg => INTERACTIVE_FLAGS.includes(arg));
7
+ export const stripGlobalFlags = (argv) => {
8
+ const kept = argv.filter(arg => !GLOBAL_FLAGS.has(arg));
9
+ if (kept.length !== argv.length) {
10
+ argv.splice(0, argv.length, ...kept);
11
+ }
12
+ };
13
+ const hook = async function (opts) {
14
+ const interactive = hasInteractiveFlag(opts.argv) || hasInteractiveFlag(process.argv);
15
+ stripGlobalFlags(opts.argv);
16
+ stripGlobalFlags(process.argv);
17
+ if (interactive) {
6
18
  setNonInteractive(true);
7
19
  }
8
20
  try {
@@ -1,9 +1,13 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
3
3
  import { checkForUpdate } from '#services/version_check.js';
4
+ import { setNonInteractive } from '#utils/interactive.js';
4
5
  vi.mock('#services/version_check.js', () => ({
5
6
  checkForUpdate: vi.fn()
6
7
  }));
8
+ vi.mock('#utils/interactive.js', () => ({
9
+ setNonInteractive: vi.fn()
10
+ }));
7
11
  vi.mock('@oclif/core', () => ({
8
12
  ux: {
9
13
  stdout: vi.fn(),
@@ -11,7 +15,7 @@ vi.mock('@oclif/core', () => ({
11
15
  }
12
16
  }));
13
17
  import { ux } from '@oclif/core';
14
- import hook from './init.js';
18
+ import hook, { hasInteractiveFlag, stripGlobalFlags } from './init.js';
15
19
  describe('init hook', () => {
16
20
  beforeEach(() => {
17
21
  vi.clearAllMocks();
@@ -26,7 +30,7 @@ describe('init hook', () => {
26
30
  latestVersion: '1.0.0'
27
31
  });
28
32
  const ctx = createHookContext();
29
- await hook.call(ctx, {});
33
+ await hook.call(ctx, { argv: [], id: undefined });
30
34
  expect(checkForUpdate).toHaveBeenCalledWith('0.8.4', '/tmp/test-cache');
31
35
  expect(ux.stdout).toHaveBeenCalled();
32
36
  const output = vi.mocked(ux.stdout).mock.calls.map(c => c[0]).join('\n');
@@ -42,13 +46,83 @@ describe('init hook', () => {
42
46
  latestVersion: '0.8.4'
43
47
  });
44
48
  const ctx = createHookContext();
45
- await hook.call(ctx, {});
49
+ await hook.call(ctx, { argv: [], id: undefined });
46
50
  expect(ux.stdout).not.toHaveBeenCalled();
47
51
  });
48
52
  it('should silently handle errors', async () => {
49
53
  vi.mocked(checkForUpdate).mockRejectedValue(new Error('network failure'));
50
54
  const ctx = createHookContext();
51
- await hook.call(ctx, {});
55
+ await hook.call(ctx, { argv: [], id: undefined });
52
56
  expect(ux.stdout).not.toHaveBeenCalled();
53
57
  });
58
+ describe('global interactive flags', () => {
59
+ const originalArgv = process.argv;
60
+ beforeEach(() => {
61
+ vi.mocked(checkForUpdate).mockResolvedValue({
62
+ updateAvailable: false,
63
+ currentVersion: '0.8.4',
64
+ latestVersion: '0.8.4'
65
+ });
66
+ });
67
+ afterEach(() => {
68
+ process.argv = originalArgv;
69
+ });
70
+ it('should mutate opts.argv in place to strip --yes', async () => {
71
+ process.argv = ['node', 'run.js', 'init', '--yes', 'my-project'];
72
+ const optsArgv = ['--yes', 'my-project'];
73
+ const argvRef = optsArgv;
74
+ const ctx = createHookContext();
75
+ await hook.call(ctx, { argv: optsArgv, id: 'init' });
76
+ expect(setNonInteractive).toHaveBeenCalledWith(true);
77
+ expect(optsArgv).toBe(argvRef);
78
+ expect(optsArgv).toEqual(['my-project']);
79
+ expect(process.argv).toEqual(['node', 'run.js', 'init', 'my-project']);
80
+ });
81
+ it('should mutate opts.argv in place to strip --non-interactive', async () => {
82
+ process.argv = ['node', 'run.js', 'init', '--non-interactive'];
83
+ const optsArgv = ['--non-interactive'];
84
+ const ctx = createHookContext();
85
+ await hook.call(ctx, { argv: optsArgv, id: 'init' });
86
+ expect(setNonInteractive).toHaveBeenCalledWith(true);
87
+ expect(optsArgv).toEqual([]);
88
+ expect(process.argv).toEqual(['node', 'run.js', 'init']);
89
+ });
90
+ it('should leave argv untouched when no global flag is present', async () => {
91
+ process.argv = ['node', 'run.js', 'init', '--skip-env'];
92
+ const optsArgv = ['--skip-env'];
93
+ const ctx = createHookContext();
94
+ await hook.call(ctx, { argv: optsArgv, id: 'init' });
95
+ expect(setNonInteractive).not.toHaveBeenCalled();
96
+ expect(optsArgv).toEqual(['--skip-env']);
97
+ expect(process.argv).toEqual(['node', 'run.js', 'init', '--skip-env']);
98
+ });
99
+ });
100
+ describe('hasInteractiveFlag', () => {
101
+ it('returns true when --yes is present', () => {
102
+ expect(hasInteractiveFlag(['init', '--yes', 'foo'])).toBe(true);
103
+ });
104
+ it('returns true when --non-interactive is present', () => {
105
+ expect(hasInteractiveFlag(['--non-interactive'])).toBe(true);
106
+ });
107
+ it('returns false for unrelated flags', () => {
108
+ expect(hasInteractiveFlag(['init', '--skip-env', '--skip-git'])).toBe(false);
109
+ });
110
+ it('returns false for an empty argv', () => {
111
+ expect(hasInteractiveFlag([])).toBe(false);
112
+ });
113
+ });
114
+ describe('stripGlobalFlags', () => {
115
+ it('mutates argv in place to remove global flags', () => {
116
+ const argv = ['init', '--yes', 'foo', '--non-interactive'];
117
+ const ref = argv;
118
+ stripGlobalFlags(argv);
119
+ expect(argv).toBe(ref);
120
+ expect(argv).toEqual(['init', 'foo']);
121
+ });
122
+ it('leaves argv untouched when no global flag is present', () => {
123
+ const argv = ['init', '--skip-env'];
124
+ stripGlobalFlags(argv);
125
+ expect(argv).toEqual(['init', '--skip-env']);
126
+ });
127
+ });
54
128
  });
@@ -27,5 +27,5 @@ export declare function createSigintHandler(projectPath: string, folderCreated:
27
27
  * @param skipEnv - Whether to skip environment configuration prompts
28
28
  * @param folderName - Optional folder name to skip folder name prompt
29
29
  */
30
- export declare function runInit(skipEnv?: boolean, folderName?: string): Promise<void>;
30
+ export declare function runInit(skipEnv?: boolean, skipGit?: boolean, folderName?: string): Promise<void>;
31
31
  export {};
@@ -119,6 +119,18 @@ async function executeNpmInstall(projectPath) {
119
119
  async function initializeAgents(projectPath) {
120
120
  await initializeAgentConfig({ projectRoot: projectPath, force: false });
121
121
  }
122
+ async function maybeInitializeGit(projectPath) {
123
+ const shouldInit = await confirm({
124
+ message: 'Initialize a git repository?',
125
+ default: true
126
+ });
127
+ if (!shouldInit) {
128
+ return false;
129
+ }
130
+ return executeCommandWithMessages(async () => {
131
+ await executeCommand('git', ['init'], projectPath);
132
+ }, 'Initializing git repository...', 'Git repository initialized');
133
+ }
122
134
  /**
123
135
  * Format error message for init errors
124
136
  * Single responsibility: only format error messages, no cleanup logic
@@ -172,7 +184,7 @@ function handleRunInitError(error, projectPath, projectFolderCreated) {
172
184
  * @param skipEnv - Whether to skip environment configuration prompts
173
185
  * @param folderName - Optional folder name to skip folder name prompt
174
186
  */
175
- export async function runInit(skipEnv = false, folderName) {
187
+ export async function runInit(skipEnv = false, skipGit = false, folderName) {
176
188
  // Track state for SIGINT cleanup using an object to avoid let
177
189
  const state = {
178
190
  projectFolderCreated: false,
@@ -210,6 +222,9 @@ export async function runInit(skipEnv = false, folderName) {
210
222
  await fs.copyFile(path.join(config.projectPath, '.env.example'), path.join(config.projectPath, '.env'));
211
223
  await executeCommandWithMessages(() => initializeAgents(config.projectPath), 'Initializing agent system...', 'Agent system initialized');
212
224
  const installSuccess = await executeCommandWithMessages(() => executeNpmInstall(config.projectPath), 'Installing dependencies...', 'Dependencies installed');
225
+ if (!skipGit) {
226
+ await maybeInitializeGit(config.projectPath);
227
+ }
213
228
  const nextSteps = getProjectSuccessMessage(config.folderName, installSuccess, credentialsConfigured);
214
229
  ux.stdout('Project created successfully!');
215
230
  ux.stdout(nextSteps);
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef, useMemo } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
- import { getWorkflowIdResult } from '#api/generated/api.js';
5
+ import { getWorkflowIdRunsRidResult } from '#api/generated/api.js';
6
6
  import { StatusIcon, statusColor } from '#components/status_icon.js';
7
7
  import { elapsedMs, formatDurationCompact } from '#utils/date_formatter.js';
8
8
  import { CommandFooter } from '#components/command_footer.js';
@@ -59,17 +59,19 @@ export const WorkflowListView = ({ runs, onBack }) => {
59
59
  const clampedIndex = Math.min(selectedIndex, Math.max(0, sortedRuns.length - 1));
60
60
  const selectedRun = sortedRuns[clampedIndex];
61
61
  const selectedWorkflowId = selectedRun?.workflowId;
62
+ const selectedRunId = selectedRun?.runId;
62
63
  useEffect(() => {
63
64
  if (clampedIndex !== selectedIndex) {
64
65
  setSelectedIndex(clampedIndex);
65
66
  }
66
67
  }, [clampedIndex, selectedIndex]);
67
68
  useEffect(() => {
68
- if (!selectedWorkflowId) {
69
+ if (!selectedWorkflowId || !selectedRunId) {
69
70
  setDetail(null);
70
71
  return;
71
72
  }
72
- const cached = cacheRef.current.get(selectedWorkflowId);
73
+ const cacheKey = `${selectedWorkflowId}:${selectedRunId}`;
74
+ const cached = cacheRef.current.get(cacheKey);
73
75
  if (cached) {
74
76
  setDetail(cached);
75
77
  setDetailLoading(false);
@@ -77,13 +79,13 @@ export const WorkflowListView = ({ runs, onBack }) => {
77
79
  }
78
80
  const currentFetchId = ++fetchIdRef.current;
79
81
  setDetailLoading(true);
80
- getWorkflowIdResult(selectedWorkflowId)
82
+ getWorkflowIdRunsRidResult(selectedWorkflowId, selectedRunId)
81
83
  .then(response => {
82
84
  if (fetchIdRef.current !== currentFetchId) {
83
85
  return;
84
86
  }
85
87
  const data = response.data;
86
- cacheRef.current.set(selectedWorkflowId, data);
88
+ cacheRef.current.set(cacheKey, data);
87
89
  setDetail(data);
88
90
  setDetailLoading(false);
89
91
  })
@@ -94,7 +96,7 @@ export const WorkflowListView = ({ runs, onBack }) => {
94
96
  setDetail(null);
95
97
  setDetailLoading(false);
96
98
  });
97
- }, [selectedWorkflowId]);
99
+ }, [selectedWorkflowId, selectedRunId]);
98
100
  useInput((input, key) => {
99
101
  if (key.upArrow) {
100
102
  setSelectedIndex(i => Math.max(0, i - 1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.2.1-next.bd54540.0",
3
+ "version": "0.2.1-next.eadab44.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,9 +36,9 @@
36
36
  "semver": "7.7.4",
37
37
  "undici": "8.0.2",
38
38
  "yaml": "^2.8.3",
39
- "@outputai/credentials": "0.2.1-next.bd54540.0",
40
- "@outputai/evals": "0.2.1-next.bd54540.0",
41
- "@outputai/llm": "0.2.1-next.bd54540.0"
39
+ "@outputai/credentials": "0.2.1-next.eadab44.0",
40
+ "@outputai/evals": "0.2.1-next.eadab44.0",
41
+ "@outputai/llm": "0.2.1-next.eadab44.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/cli-progress": "3.11.6",