runwork 0.10.0 → 0.10.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { __testing } from '../info.js';
3
+ const { mergeSources, padName } = __testing;
4
+ describe('mergeSources — bad data resilience', () => {
5
+ it('drops blueprint entries with missing name', () => {
6
+ const map = mergeSources([
7
+ { name: 'good', extra: {} },
8
+ { name: undefined, extra: {} },
9
+ { name: '', extra: {} },
10
+ ], [], (item) => item.name ?? '', 'app-1');
11
+ expect(Array.from(map.keys())).toEqual(['good']);
12
+ });
13
+ it('drops server items whose name resolver returns undefined or empty', () => {
14
+ const map = mergeSources([], [
15
+ { name: 'good', appId: 'app-1', appName: 'My App', deploymentMode: 'preview' },
16
+ { name: undefined, appId: 'app-1', appName: 'My App', deploymentMode: 'preview' },
17
+ { name: '', appId: 'app-1', appName: 'My App', deploymentMode: 'preview' },
18
+ ], (item) => item.name ?? '', 'app-1');
19
+ expect(Array.from(map.keys())).toEqual(['good']);
20
+ });
21
+ it('skips server items belonging to other apps', () => {
22
+ const map = mergeSources([], [
23
+ { name: 'mine', appId: 'app-1', appName: 'A', deploymentMode: 'preview' },
24
+ { name: 'theirs', appId: 'app-2', appName: 'B', deploymentMode: 'preview' },
25
+ ], (item) => item.name ?? '', 'app-1');
26
+ expect(Array.from(map.keys())).toEqual(['mine']);
27
+ });
28
+ it('merges blueprint and server entries by name', () => {
29
+ const map = mergeSources([{ name: 'agent-x', extra: { type: 'task' } }], [
30
+ { name: 'agent-x', appId: 'app-1', appName: 'A', deploymentMode: 'production' },
31
+ { name: 'agent-y', appId: 'app-1', appName: 'A', deploymentMode: 'preview' },
32
+ ], (item) => item.name ?? '', 'app-1');
33
+ const x = map.get('agent-x');
34
+ const y = map.get('agent-y');
35
+ expect(x?.sources).toEqual(['blueprint', 'production']);
36
+ expect(x?.blueprintExtra).toEqual({ type: 'task' });
37
+ expect(y?.sources).toEqual(['preview']);
38
+ });
39
+ });
40
+ describe('padName — defensive printer', () => {
41
+ it('pads valid names to the requested width', () => {
42
+ expect(padName('foo', 8)).toBe('foo ');
43
+ });
44
+ it('returns a placeholder for undefined / null / empty inputs', () => {
45
+ expect(padName(undefined, 12)).toContain('(unnamed)');
46
+ expect(padName(null, 12)).toContain('(unnamed)');
47
+ expect(padName('', 12)).toContain('(unnamed)');
48
+ // Placeholder is also padded to the requested width.
49
+ expect(padName(undefined, 12).length).toBe(12);
50
+ });
51
+ it('does not throw on non-string inputs', () => {
52
+ expect(() => padName(42, 8)).not.toThrow();
53
+ expect(() => padName({}, 8)).not.toThrow();
54
+ });
55
+ });
@@ -1,3 +1,144 @@
1
1
  import { Command } from 'commander';
2
+ import type { WorkspaceAllData } from '../types.js';
3
+ interface BlueprintEntity {
4
+ entityName: string;
5
+ schema?: Record<string, unknown>;
6
+ }
7
+ interface BlueprintSchedule {
8
+ name: string;
9
+ schedule?: string;
10
+ description?: string;
11
+ }
12
+ interface BlueprintWorkflow {
13
+ name: string;
14
+ description?: string;
15
+ }
16
+ interface BlueprintAgent {
17
+ name: string;
18
+ type?: string;
19
+ description?: string;
20
+ }
21
+ interface BlueprintEndpoint {
22
+ path: string;
23
+ method?: string;
24
+ description?: string;
25
+ }
26
+ interface BlueprintComponent {
27
+ componentName: string;
28
+ }
29
+ interface Blueprint {
30
+ entities?: BlueprintEntity[];
31
+ schedules?: BlueprintSchedule[];
32
+ scheduledJobs?: BlueprintSchedule[];
33
+ workflows?: BlueprintWorkflow[];
34
+ agents?: BlueprintAgent[];
35
+ endpoints?: BlueprintEndpoint[];
36
+ publicEndpoints?: BlueprintEndpoint[];
37
+ components?: BlueprintComponent[];
38
+ fileStorage?: boolean | {
39
+ enabled?: boolean;
40
+ };
41
+ }
42
+ interface MergedEntity {
43
+ name: string;
44
+ sources: string[];
45
+ schema?: Record<string, unknown>;
46
+ }
47
+ interface MergedScheduledJob {
48
+ name: string;
49
+ sources: string[];
50
+ schedule?: string;
51
+ description?: string;
52
+ }
53
+ interface MergedWorkflow {
54
+ name: string;
55
+ sources: string[];
56
+ description?: string;
57
+ }
58
+ interface MergedAgent {
59
+ name: string;
60
+ sources: string[];
61
+ type?: string;
62
+ description?: string;
63
+ }
64
+ interface MergedEndpoint {
65
+ path: string;
66
+ method?: string;
67
+ sources: string[];
68
+ description?: string;
69
+ }
70
+ interface MergedComponent {
71
+ name: string;
72
+ sources: string[];
73
+ }
74
+ interface MergedFileStorage {
75
+ enabled: boolean;
76
+ sources: string[];
77
+ }
78
+ interface InfoOutput {
79
+ app: {
80
+ id: string;
81
+ name: string;
82
+ slug: string;
83
+ } | null;
84
+ workspace: {
85
+ id: string;
86
+ name: string;
87
+ };
88
+ preview: {
89
+ url: string | null;
90
+ active: boolean;
91
+ };
92
+ production: {
93
+ url: null;
94
+ deployed: boolean;
95
+ };
96
+ integrations: Array<{
97
+ id: string;
98
+ status: string;
99
+ }> | null;
100
+ registries: {
101
+ entities: MergedEntity[];
102
+ workflows: MergedWorkflow[];
103
+ scheduledJobs: MergedScheduledJob[];
104
+ agents: MergedAgent[];
105
+ publicEndpoints: MergedEndpoint[];
106
+ components: MergedComponent[];
107
+ fileStorage: MergedFileStorage;
108
+ } | null;
109
+ cli: {
110
+ version: string;
111
+ commands: Array<{
112
+ name: string;
113
+ description: string;
114
+ }>;
115
+ };
116
+ }
117
+ interface ServerRegistryItem {
118
+ appId: string;
119
+ appName: string;
120
+ deploymentMode?: 'preview' | 'production';
121
+ }
122
+ declare function mergeSources<T extends ServerRegistryItem>(blueprintItems: Array<{
123
+ name: string;
124
+ extra?: Record<string, unknown>;
125
+ }>, serverItems: T[], getServerName: (item: T) => string, appId: string): Map<string, {
126
+ sources: string[];
127
+ serverItem?: T;
128
+ blueprintExtra?: Record<string, unknown>;
129
+ }>;
130
+ declare function buildRegistries(blueprint: Blueprint | null, serverData: WorkspaceAllData | null, appId: string): InfoOutput['registries'];
131
+ /**
132
+ * Pad a possibly-missing string to a fixed width. Returns a visible
133
+ * placeholder when the value isn't a usable string so the human-readable
134
+ * printer can never crash on an unexpectedly-undefined field.
135
+ */
136
+ declare function padName(value: unknown, width: number): string;
2
137
  export declare function formatSources(sources: string[]): string;
138
+ export declare const __testing: {
139
+ mergeSources: typeof mergeSources;
140
+ padName: typeof padName;
141
+ buildRegistries: typeof buildRegistries;
142
+ };
3
143
  export declare const infoCommand: Command;
144
+ export {};
@@ -32,13 +32,22 @@ function readBlueprint(cwd) {
32
32
  }
33
33
  function mergeSources(blueprintItems, serverItems, getServerName, appId) {
34
34
  const map = new Map();
35
+ // Skip nameless entries so we never store an `undefined` key. A bad
36
+ // blueprint or a partially-populated server-side row would otherwise
37
+ // produce a registry entry with `name === undefined`, which then crashes
38
+ // the human-readable printer at `a.name.padEnd(24)` and yields ugly
39
+ // `"undefined": ...` keys in JSON output.
35
40
  for (const bp of blueprintItems) {
41
+ if (typeof bp.name !== 'string' || bp.name.length === 0)
42
+ continue;
36
43
  map.set(bp.name, { sources: ['blueprint'], blueprintExtra: bp.extra });
37
44
  }
38
45
  for (const serverItem of serverItems) {
39
46
  if (serverItem.appId !== appId)
40
47
  continue;
41
48
  const name = getServerName(serverItem);
49
+ if (typeof name !== 'string' || name.length === 0)
50
+ continue;
42
51
  const mode = serverItem.deploymentMode;
43
52
  const source = mode === 'production' ? 'production' : 'preview';
44
53
  const existing = map.get(name);
@@ -171,6 +180,16 @@ function getCliCommands(command) {
171
180
  return { name, description: cmd.description() };
172
181
  });
173
182
  }
183
+ /**
184
+ * Pad a possibly-missing string to a fixed width. Returns a visible
185
+ * placeholder when the value isn't a usable string so the human-readable
186
+ * printer can never crash on an unexpectedly-undefined field.
187
+ */
188
+ function padName(value, width) {
189
+ if (typeof value === 'string' && value.length > 0)
190
+ return value.padEnd(width);
191
+ return '(unnamed)'.padEnd(width);
192
+ }
174
193
  export function formatSources(sources) {
175
194
  return sources
176
195
  .map(s => {
@@ -240,7 +259,7 @@ function printHumanOutput(data) {
240
259
  if (reg.entities.length > 0) {
241
260
  console.log(` ${bold('Entities:')}`);
242
261
  for (const e of reg.entities) {
243
- console.log(` ${e.name.padEnd(24)} ${formatSources(e.sources)}`);
262
+ console.log(` ${padName(e.name, 24)} ${formatSources(e.sources)}`);
244
263
  }
245
264
  console.log('');
246
265
  }
@@ -248,7 +267,7 @@ function printHumanOutput(data) {
248
267
  console.log(` ${bold('Workflows:')}`);
249
268
  for (const w of reg.workflows) {
250
269
  const desc = w.description ? ` ${dim(w.description)}` : '';
251
- console.log(` ${w.name.padEnd(24)} ${formatSources(w.sources)}${desc}`);
270
+ console.log(` ${padName(w.name, 24)} ${formatSources(w.sources)}${desc}`);
252
271
  }
253
272
  console.log('');
254
273
  }
@@ -256,7 +275,7 @@ function printHumanOutput(data) {
256
275
  console.log(` ${bold('Scheduled Jobs:')}`);
257
276
  for (const s of reg.scheduledJobs) {
258
277
  const schedule = s.schedule ? ` ${gray(s.schedule)}` : '';
259
- console.log(` ${s.name.padEnd(24)} ${formatSources(s.sources)}${schedule}`);
278
+ console.log(` ${padName(s.name, 24)} ${formatSources(s.sources)}${schedule}`);
260
279
  }
261
280
  console.log('');
262
281
  }
@@ -264,22 +283,22 @@ function printHumanOutput(data) {
264
283
  console.log(` ${bold('Agents:')}`);
265
284
  for (const a of reg.agents) {
266
285
  const type = a.type ? ` ${dim(a.type)}` : '';
267
- console.log(` ${a.name.padEnd(24)} ${formatSources(a.sources)}${type}`);
286
+ console.log(` ${padName(a.name, 24)} ${formatSources(a.sources)}${type}`);
268
287
  }
269
288
  console.log('');
270
289
  }
271
290
  if (reg.publicEndpoints.length > 0) {
272
291
  console.log(` ${bold('Public Endpoints:')}`);
273
292
  for (const e of reg.publicEndpoints) {
274
- const label = e.method ? `${e.method} ${e.path}` : e.path;
275
- console.log(` ${label.padEnd(24)} ${formatSources(e.sources)}`);
293
+ const label = e.method && e.path ? `${e.method} ${e.path}` : (e.path || '');
294
+ console.log(` ${padName(label, 24)} ${formatSources(e.sources)}`);
276
295
  }
277
296
  console.log('');
278
297
  }
279
298
  if (reg.components.length > 0) {
280
299
  console.log(` ${bold('Components:')}`);
281
300
  for (const c of reg.components) {
282
- console.log(` ${c.name.padEnd(24)} ${formatSources(c.sources)}`);
301
+ console.log(` ${padName(c.name, 24)} ${formatSources(c.sources)}`);
283
302
  }
284
303
  console.log('');
285
304
  }
@@ -298,6 +317,9 @@ function printHumanOutput(data) {
298
317
  console.log(dim(` CLI version: ${data.cli.version}`));
299
318
  console.log('');
300
319
  }
320
+ // Internal helpers exposed for unit tests; do not import from anywhere
321
+ // other than `__tests__/`.
322
+ export const __testing = { mergeSources, padName, buildRegistries };
301
323
  export const infoCommand = new Command('info')
302
324
  .description('Show app context, registries, and CLI reference (use --json for agent discovery)')
303
325
  .action(async (_opts, command) => {
@@ -1 +1 @@
1
- export declare const VERSION = "0.10.0";
1
+ export declare const VERSION = "0.10.1";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-types.ts -- do not edit
2
- export const VERSION = "0.10.0";
2
+ export const VERSION = "0.10.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",