@outputai/cli 0.15.1-next.5db73d3.0 → 0.15.1-next.755b932.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.
@@ -80,7 +80,7 @@ services:
80
80
  condition: service_healthy
81
81
  worker:
82
82
  condition: service_healthy
83
- image: outputai/api:${OUTPUT_API_VERSION:-0.15.1-next.5db73d3.0}
83
+ image: outputai/api:${OUTPUT_API_VERSION:-0.15.1-next.755b932.0}
84
84
  init: true
85
85
  networks:
86
86
  - main
@@ -9,7 +9,8 @@ const Ansi = {
9
9
  RESET: '\x1b[0m'
10
10
  };
11
11
  export default class Fix extends Command {
12
- static description = 'Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated) and remove stale hook files';
12
+ static description = 'Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated), ' +
13
+ 'replace the @outputai/output package with @outputai/core and @outputai/cli, and remove stale hook files';
13
14
  static examples = [
14
15
  '<%= config.bin %> <%= command.id %>'
15
16
  ];
@@ -30,6 +31,11 @@ export default class Fix extends Command {
30
31
  this.log('\n Scripts to remove:');
31
32
  plan.scriptsToRemove.forEach(({ key }) => this.log(` ${Ansi.RED}-${Ansi.RESET} "${key}"`));
32
33
  }
34
+ if (plan.dependenciesToRemove.length > 0 || plan.dependenciesToAdd.length > 0) {
35
+ this.log('\n Dependencies to replace:');
36
+ plan.dependenciesToRemove.forEach(({ section, name, version }) => this.log(` ${Ansi.RED}-${Ansi.RESET} "${name}": "${version}" (${section})`));
37
+ plan.dependenciesToAdd.forEach(({ section, name, version }) => this.log(` ${Ansi.GREEN}+${Ansi.RESET} "${name}": "${version}" (${section})`));
38
+ }
33
39
  if (plan.hookFilesToRemove.length > 0) {
34
40
  this.log('\n Hook files to remove:');
35
41
  plan.hookFilesToRemove.forEach(file => this.log(` ${Ansi.RED}-${Ansi.RESET} "${file}"`));
@@ -51,6 +57,9 @@ export default class Fix extends Command {
51
57
  }
52
58
  applyFix(plan);
53
59
  this.log('Done, package.json is properly configured.');
60
+ if (plan.dependenciesToRemove.length > 0) {
61
+ this.log('Dependencies changed: run your package manager install to update node_modules and the lockfile.');
62
+ }
54
63
  }
55
64
  catch (error) {
56
65
  // ExitPromptError means Ctrl+C
@@ -17,6 +17,8 @@ const basePlan = () => ({
17
17
  scriptsToRemove: [{ key: 'dev', value: 'old' }],
18
18
  scriptsToAdd: [{ key: 'output:new', value: 'echo new' }],
19
19
  scriptsToReplace: [{ key: 'output:dev', before: 'old dev', after: 'output dev' }],
20
+ dependenciesToRemove: [],
21
+ dependenciesToAdd: [],
20
22
  hookFilesToRemove: []
21
23
  });
22
24
  describe('fix command', () => {
@@ -71,6 +73,34 @@ describe('fix command', () => {
71
73
  expect(cmd.log).toHaveBeenCalledWith('\n Hook files to remove:');
72
74
  expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('"node_modules/@outputai/credentials/dist/hooks.js"'));
73
75
  });
76
+ it('should list dependencies to replace and hint to reinstall after applying', async () => {
77
+ vi.mocked(fixService.planFix).mockReturnValue({
78
+ ...basePlan(),
79
+ dependenciesToRemove: [{ section: 'dependencies', name: '@outputai/output', version: '^0.15.0' }],
80
+ dependenciesToAdd: [
81
+ { section: 'dependencies', name: '@outputai/core', version: '^0.15.0' },
82
+ { section: 'devDependencies', name: '@outputai/cli', version: '^0.15.0' }
83
+ ]
84
+ });
85
+ vi.mocked(confirm).mockResolvedValue(true);
86
+ vi.mocked(fixService.applyFix).mockImplementation(() => { });
87
+ const cmd = createTestCommand();
88
+ await cmd.run();
89
+ expect(cmd.log).toHaveBeenCalledWith('\n Dependencies to replace:');
90
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('"@outputai/output": "^0.15.0" (dependencies)'));
91
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('"@outputai/core": "^0.15.0" (dependencies)'));
92
+ expect(cmd.log).toHaveBeenCalledWith(expect.stringContaining('"@outputai/cli": "^0.15.0" (devDependencies)'));
93
+ expect(cmd.log).toHaveBeenCalledWith('Dependencies changed: run your package manager install to update node_modules and the lockfile.');
94
+ });
95
+ it('should not hint to reinstall when dependencies did not change', async () => {
96
+ vi.mocked(fixService.planFix).mockReturnValue(basePlan());
97
+ vi.mocked(confirm).mockResolvedValue(true);
98
+ vi.mocked(fixService.applyFix).mockImplementation(() => { });
99
+ const cmd = createTestCommand();
100
+ await cmd.run();
101
+ expect(cmd.log).not.toHaveBeenCalledWith('\n Dependencies to replace:');
102
+ expect(cmd.log).not.toHaveBeenCalledWith(expect.stringContaining('Dependencies changed'));
103
+ });
74
104
  it('should not apply when user declines', async () => {
75
105
  vi.mocked(fixService.planFix).mockReturnValue(basePlan());
76
106
  vi.mocked(confirm).mockResolvedValue(false);
@@ -103,8 +103,8 @@ export default class Update extends Command {
103
103
  if (newLocalVersion) {
104
104
  this.log(`\nLocal install updated to v${newLocalVersion}`);
105
105
  if (isOutdated(newLocalVersion, latest)) {
106
- this.warn(`Your package.json constrains @outputai/output which limits @outputai/cli to v${newLocalVersion}. ` +
107
- 'Update the @outputai/output version range in package.json to get the latest CLI.');
106
+ this.warn(`Your package.json version range limits @outputai/cli to v${newLocalVersion}. ` +
107
+ 'Update the range in package.json to get the latest CLI.');
108
108
  }
109
109
  }
110
110
  else {
@@ -172,7 +172,7 @@ describe('update command', () => {
172
172
  .mockReturnValueOnce(true);
173
173
  const cmd = createTestCommand({ cli: true });
174
174
  await cmd.run();
175
- expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('package.json constrains'));
175
+ expect(cmd.warn).toHaveBeenCalledWith(expect.stringContaining('package.json version range limits'));
176
176
  });
177
177
  });
178
178
  });
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.15.1-next.5db73d3.0"
2
+ "framework": "0.15.1-next.755b932.0"
3
3
  }
@@ -286,41 +286,41 @@ describe('findLLMCalls', () => {
286
286
  const items = [
287
287
  { group: 'input', label: 'no_cache', amount: 1032, ppm: 0.3, total: 0.0003096, status: 'ok' },
288
288
  { group: 'output', label: 'text', amount: 793, ppm: 2.5, total: 0.0019825, status: 'ok' },
289
- { group: 'tools', label: 'grounding_prompt', amount: 1, ppm: 35_000, total: 0.035, status: 'ok' }
289
+ { group: 'tools', label: 'grounding_query', amount: 3, ppm: 14_000, total: 0.042, status: 'ok' }
290
290
  ];
291
291
  const calls = findLLMCalls({
292
292
  kind: 'workflow',
293
- children: [llmCostNode('grounded', 'gemini-2.5-flash', items)]
293
+ children: [llmCostNode('grounded', 'gemini-3.1-flash-lite', items)]
294
294
  });
295
295
  expect(calls[0].incomplete).toBe(false);
296
- expect(calls[0].originalCost).toBeCloseTo(0.0372921, 8);
297
- // Grounding bills per request, so it must not inflate token usage.
296
+ expect(calls[0].originalCost).toBeCloseTo(0.0442921, 8);
297
+ // Grounding bills per query, so it must not inflate token usage.
298
298
  expect(calls[0].usage).toEqual({
299
299
  inputTokens: 1032,
300
300
  cachedInputTokens: 0,
301
301
  outputTokens: 793,
302
302
  reasoningTokens: 0
303
303
  });
304
- expect(calls[0].lines.find(l => l.type === 'tools_grounding_prompt')).toEqual({
305
- type: 'tools_grounding_prompt',
306
- ppm: 35_000,
307
- amount: 1,
308
- total: 0.035
304
+ expect(calls[0].lines.find(l => l.type === 'tools_grounding_query')).toEqual({
305
+ type: 'tools_grounding_query',
306
+ ppm: 14_000,
307
+ amount: 3,
308
+ total: 0.042
309
309
  });
310
310
  });
311
311
  it('preserves a priced grounding charge as-charged when re-pricing at costs.yml rates', () => {
312
312
  const items = [
313
313
  { group: 'input', label: 'no_cache', amount: 1032, ppm: 0.3, total: 0.0003096, status: 'ok' },
314
314
  { group: 'output', label: 'text', amount: 793, ppm: 2.5, total: 0.0019825, status: 'ok' },
315
- { group: 'tools', label: 'grounding_prompt', amount: 1, ppm: 35_000, total: 0.035, status: 'ok' }
315
+ { group: 'tools', label: 'grounding_query', amount: 3, ppm: 14_000, total: 0.042, status: 'ok' }
316
316
  ];
317
317
  const config = {
318
- models: { 'gemini-2.5-flash': { provider: 'google-vertex', input: 0.3, output: 2.5 } },
318
+ models: { 'gemini-3.1-flash-lite': { provider: 'google-vertex', input: 0.3, output: 2.5 } },
319
319
  services: {}
320
320
  };
321
- const report = calculateCost({ kind: 'workflow', name: 'w', children: [llmCostNode('grounded', 'gemini-2.5-flash', items)] }, config);
321
+ const report = calculateCost({ kind: 'workflow', name: 'w', children: [llmCostNode('grounded', 'gemini-3.1-flash-lite', items)] }, config);
322
322
  // grounding has no costs.yml line rate, so it degrades to its as-charged total, not $0.
323
- expect(report.llmAdjustedCost).toBeCloseTo(0.0372921, 8);
323
+ expect(report.llmAdjustedCost).toBeCloseTo(0.0442921, 8);
324
324
  });
325
325
  it('flags an unrated grounding charge as incomplete instead of a silent $0', () => {
326
326
  const items = [
@@ -1,5 +1,7 @@
1
1
  /** Legacy script names from older versions of the project */
2
2
  export declare const legacyScripts: readonly ["dev"];
3
+ declare const dependencySections: readonly ["dependencies", "devDependencies"];
4
+ type DependencySection = typeof dependencySections[number];
3
5
  export interface ScriptToRemove {
4
6
  key: string;
5
7
  value: string;
@@ -13,8 +15,14 @@ export interface ScriptToReplace {
13
15
  before: string;
14
16
  after: string;
15
17
  }
18
+ export interface DependencyChange {
19
+ section: DependencySection;
20
+ name: string;
21
+ version: string;
22
+ }
16
23
  /**
17
- * Plan for aligning `scripts` with the scaffold template and dropping stale hook files (reads only; no write until apply).
24
+ * Plan for aligning `scripts` with the scaffold template, replacing the `@outputai/output` umbrella package
25
+ * and dropping stale hook files (reads only; no write until apply).
18
26
  */
19
27
  export interface FixPlan {
20
28
  packageJsonPath: string;
@@ -23,6 +31,8 @@ export interface FixPlan {
23
31
  scriptsToRemove: ScriptToRemove[];
24
32
  scriptsToReplace: ScriptToReplace[];
25
33
  scriptsToAdd: ScriptToAdd[];
34
+ dependenciesToRemove: DependencyChange[];
35
+ dependenciesToAdd: DependencyChange[];
26
36
  hookFilesToRemove: string[];
27
37
  }
28
38
  /**
@@ -33,3 +43,4 @@ export declare function planFix(projectRoot: string): FixPlan;
33
43
  * Writes the planned package.json. No-op when {@link FixPlan.hasChanges} is false.
34
44
  */
35
45
  export declare function applyFix(plan: FixPlan): void;
46
+ export {};
@@ -8,6 +8,13 @@ const templateRelativePath = path.join('templates', 'project', 'package.json.tem
8
8
  export const legacyScripts = ['dev'];
9
9
  /** Hook files from the @outputai/credentials package, which was folded into @outputai/core */
10
10
  const credentialsHookFilePattern = /@outputai[\\/]credentials([\\/]|$)/;
11
+ /** The @outputai/output umbrella package is replaced by its parts: core at runtime, the CLI for development */
12
+ const umbrellaPackage = '@outputai/output';
13
+ const umbrellaReplacements = [
14
+ { name: '@outputai/core', section: 'dependencies' },
15
+ { name: '@outputai/cli', section: 'devDependencies' }
16
+ ];
17
+ const dependencySections = ['dependencies', 'devDependencies'];
11
18
  function getTemplatesPackageJsonPath() {
12
19
  const __filename = fileURLToPath(import.meta.url);
13
20
  const __dirname = path.dirname(__filename);
@@ -46,6 +53,31 @@ function removeCredentialsHookFiles(pkg) {
46
53
  section.hookFiles = hookFiles.filter(file => !removed.includes(file));
47
54
  return removed;
48
55
  }
56
+ function getDependencySection(pkg, section) {
57
+ const deps = pkg[section];
58
+ return typeof deps === 'object' && deps !== null && !Array.isArray(deps) ? deps : null;
59
+ }
60
+ function replaceUmbrellaPackage(pkg) {
61
+ const dependenciesToRemove = [];
62
+ const dependenciesToAdd = [];
63
+ const section = dependencySections.find(s => typeof getDependencySection(pkg, s)?.[umbrellaPackage] === 'string');
64
+ if (!section) {
65
+ return { dependenciesToRemove, dependenciesToAdd };
66
+ }
67
+ const deps = getDependencySection(pkg, section);
68
+ const version = deps[umbrellaPackage];
69
+ delete deps[umbrellaPackage];
70
+ dependenciesToRemove.push({ section, name: umbrellaPackage, version });
71
+ const isDeclared = (name) => dependencySections.some(s => Object.hasOwn(getDependencySection(pkg, s) ?? {}, name));
72
+ for (const { name, section: target } of umbrellaReplacements) {
73
+ if (isDeclared(name)) {
74
+ continue;
75
+ }
76
+ pkg[target] = { ...getDependencySection(pkg, target), [name]: version };
77
+ dependenciesToAdd.push({ section: target, name, version });
78
+ }
79
+ return { dependenciesToRemove, dependenciesToAdd };
80
+ }
49
81
  function parseTemplatePackageJson(processed) {
50
82
  try {
51
83
  return JSON.parse(processed);
@@ -95,6 +127,7 @@ export function planFix(projectRoot) {
95
127
  scripts[key] = after;
96
128
  }
97
129
  pkg.scripts = scripts;
130
+ const { dependenciesToRemove, dependenciesToAdd } = replaceUmbrellaPackage(pkg);
98
131
  const hookFilesToRemove = removeCredentialsHookFiles(pkg);
99
132
  const packageJsonUpdatedContent = `${JSON.stringify(pkg, null, 2)}\n`;
100
133
  const hasChanges = packageJsonUpdatedContent !== raw;
@@ -105,6 +138,8 @@ export function planFix(projectRoot) {
105
138
  scriptsToRemove,
106
139
  scriptsToReplace,
107
140
  scriptsToAdd,
141
+ dependenciesToRemove,
142
+ dependenciesToAdd,
108
143
  hookFilesToRemove
109
144
  };
110
145
  }
@@ -84,6 +84,46 @@ describe('fix package', () => {
84
84
  expect(malformedPlan.hookFilesToRemove).toEqual([]);
85
85
  expect(JSON.parse(malformedPlan.packageJsonUpdatedContent).outputai).toEqual(malformed.outputai);
86
86
  });
87
+ it('should replace @outputai/output in dependencies with core and a cli dev dependency at the same version', async () => {
88
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
89
+ const pkg = { name: 'my-proj', dependencies: { '@outputai/output': '^0.15.0', zod: '4.0.0' } };
90
+ await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf-8');
91
+ const plan = planFix(tmpDir);
92
+ expect(plan.dependenciesToRemove).toEqual([{ section: 'dependencies', name: '@outputai/output', version: '^0.15.0' }]);
93
+ expect(plan.dependenciesToAdd).toEqual([
94
+ { section: 'dependencies', name: '@outputai/core', version: '^0.15.0' },
95
+ { section: 'devDependencies', name: '@outputai/cli', version: '^0.15.0' }
96
+ ]);
97
+ applyFix(plan);
98
+ const next = JSON.parse(await fs.readFile(path.join(tmpDir, 'package.json'), 'utf-8'));
99
+ expect(next.dependencies).toEqual({ zod: '4.0.0', '@outputai/core': '^0.15.0' });
100
+ expect(next.devDependencies).toEqual({ '@outputai/cli': '^0.15.0' });
101
+ });
102
+ it('should replace @outputai/output in devDependencies and keep already declared replacements', async () => {
103
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
104
+ const pkg = {
105
+ name: 'my-proj',
106
+ dependencies: { '@outputai/core': '0.15.0' },
107
+ devDependencies: { '@outputai/output': '^0.15.0' }
108
+ };
109
+ await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf-8');
110
+ const plan = planFix(tmpDir);
111
+ expect(plan.dependenciesToRemove).toEqual([{ section: 'devDependencies', name: '@outputai/output', version: '^0.15.0' }]);
112
+ expect(plan.dependenciesToAdd).toEqual([{ section: 'devDependencies', name: '@outputai/cli', version: '^0.15.0' }]);
113
+ const next = JSON.parse(plan.packageJsonUpdatedContent);
114
+ expect(next.dependencies).toEqual({ '@outputai/core': '0.15.0' });
115
+ expect(next.devDependencies).toEqual({ '@outputai/cli': '^0.15.0' });
116
+ });
117
+ it('should leave dependencies untouched when @outputai/output is not declared', async () => {
118
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
119
+ const pkg = { name: 'my-proj', dependencies: { '@outputai/core': '0.15.0' }, devDependencies: { '@outputai/cli': '0.15.0' } };
120
+ await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf-8');
121
+ const plan = planFix(tmpDir);
122
+ expect(plan.dependenciesToRemove).toEqual([]);
123
+ expect(plan.dependenciesToAdd).toEqual([]);
124
+ expect(JSON.parse(plan.packageJsonUpdatedContent).dependencies).toEqual(pkg.dependencies);
125
+ expect(JSON.parse(plan.packageJsonUpdatedContent).devDependencies).toEqual(pkg.devDependencies);
126
+ });
87
127
  it('should throw when package.json is missing', async () => {
88
128
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fix-test-'));
89
129
  expect(() => planFix(tmpDir)).toThrow(/No package\.json found/);
@@ -16,9 +16,10 @@
16
16
  "dependencies": {
17
17
  "@ai-sdk/anthropic": "4.0.61",
18
18
  "@ai-sdk/openai": "4.0.73",
19
- "@outputai/output": "{{frameworkVersion}}"
19
+ "@outputai/core": "{{frameworkVersion}}"
20
20
  },
21
21
  "devDependencies": {
22
+ "@outputai/cli": "{{frameworkVersion}}",
22
23
  "@types/node": "24.5.2",
23
24
  "nodemon": "3.1.14",
24
25
  "typescript": "5.9.3"
@@ -3,7 +3,7 @@
3
3
  "fix": {
4
4
  "aliases": [],
5
5
  "args": {},
6
- "description": "Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated) and remove stale hook files",
6
+ "description": "Fix Output scripts in the package.json (reset overwrites, add missing and remove deprecated), replace the @outputai/output package with @outputai/core and @outputai/cli, and remove stale hook files",
7
7
  "examples": [
8
8
  "<%= config.bin %> <%= command.id %>"
9
9
  ],
@@ -1772,5 +1772,5 @@
1772
1772
  ]
1773
1773
  }
1774
1774
  },
1775
- "version": "0.15.1-next.5db73d3.0"
1775
+ "version": "0.15.1-next.755b932.0"
1776
1776
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.15.1-next.5db73d3.0",
3
+ "version": "0.15.1-next.755b932.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "react": "19.3.0",
38
38
  "semver": "7.8.5",
39
39
  "undici": "8.10.2",
40
- "@outputai/core": "0.15.1-next.5db73d3.0"
40
+ "@outputai/core": "0.15.1-next.755b932.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/cli-progress": "3.11.6",