runwork 0.2.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/runwork.js +2 -0
  4. package/bundled-types/agents.d.ts +3 -0
  5. package/bundled-types/ai.d.ts +2 -0
  6. package/bundled-types/app-registry.d.ts +74 -0
  7. package/bundled-types/channels.d.ts +2 -0
  8. package/bundled-types/components.d.ts +1 -0
  9. package/bundled-types/core-agent.d.ts +265 -0
  10. package/bundled-types/core-ai.d.ts +170 -0
  11. package/bundled-types/core-base-agent.d.ts +89 -0
  12. package/bundled-types/core-channels.d.ts +42 -0
  13. package/bundled-types/core-components.d.ts +21 -0
  14. package/bundled-types/core-endpoints.d.ts +198 -0
  15. package/bundled-types/core-entities.d.ts +276 -0
  16. package/bundled-types/core-entity-do.d.ts +119 -0
  17. package/bundled-types/core-events.d.ts +52 -0
  18. package/bundled-types/core-file-storage.d.ts +216 -0
  19. package/bundled-types/core-integration-entities.d.ts +147 -0
  20. package/bundled-types/core-integrations.d.ts +143 -0
  21. package/bundled-types/core-routes.d.ts +33 -0
  22. package/bundled-types/core-scheduler.d.ts +180 -0
  23. package/bundled-types/core-utils.d.ts +85 -0
  24. package/bundled-types/core-workflow-cloudflare.d.ts +85 -0
  25. package/bundled-types/core-workflow-config.d.ts +18 -0
  26. package/bundled-types/core-workflow-coordinator.d.ts +78 -0
  27. package/bundled-types/core-workflow-instance.d.ts +82 -0
  28. package/bundled-types/core-workflow-types.d.ts +251 -0
  29. package/bundled-types/core-workflows.d.ts +75 -0
  30. package/bundled-types/core-workspace.d.ts +454 -0
  31. package/bundled-types/create-app.d.ts +57 -0
  32. package/bundled-types/endpoints.d.ts +2 -0
  33. package/bundled-types/entities.d.ts +5 -0
  34. package/bundled-types/events.d.ts +2 -0
  35. package/bundled-types/index.d.ts +19 -0
  36. package/bundled-types/integrations.d.ts +2 -0
  37. package/bundled-types/routes.d.ts +2 -0
  38. package/bundled-types/scheduler.d.ts +2 -0
  39. package/bundled-types/storage.d.ts +2 -0
  40. package/bundled-types/test-entry.d.ts +21 -0
  41. package/bundled-types/types.d.ts +1 -0
  42. package/bundled-types/vite.d.ts +23 -0
  43. package/bundled-types/workflows.d.ts +14 -0
  44. package/bundled-types/workspace.d.ts +2 -0
  45. package/dist/__tests__/types-manager.test.d.ts +1 -0
  46. package/dist/__tests__/types-manager.test.js +85 -0
  47. package/dist/api/__tests__/client.test.d.ts +1 -0
  48. package/dist/api/__tests__/client.test.js +264 -0
  49. package/dist/api/client.d.ts +72 -0
  50. package/dist/api/client.js +144 -0
  51. package/dist/auth/__tests__/store.test.d.ts +1 -0
  52. package/dist/auth/__tests__/store.test.js +103 -0
  53. package/dist/auth/browser.d.ts +2 -0
  54. package/dist/auth/browser.js +7 -0
  55. package/dist/auth/store.d.ts +5 -0
  56. package/dist/auth/store.js +33 -0
  57. package/dist/commands/clone.d.ts +2 -0
  58. package/dist/commands/clone.js +90 -0
  59. package/dist/commands/deploy.d.ts +2 -0
  60. package/dist/commands/deploy.js +43 -0
  61. package/dist/commands/dev.d.ts +2 -0
  62. package/dist/commands/dev.js +188 -0
  63. package/dist/commands/init.d.ts +2 -0
  64. package/dist/commands/init.js +87 -0
  65. package/dist/commands/login.d.ts +2 -0
  66. package/dist/commands/login.js +32 -0
  67. package/dist/commands/logout.d.ts +2 -0
  68. package/dist/commands/logout.js +13 -0
  69. package/dist/commands/logs.d.ts +2 -0
  70. package/dist/commands/logs.js +166 -0
  71. package/dist/generated/bundled-types.d.ts +1 -0
  72. package/dist/generated/bundled-types.js +44 -0
  73. package/dist/generated/version.d.ts +1 -0
  74. package/dist/generated/version.js +2 -0
  75. package/dist/git/__tests__/credentials.test.d.ts +1 -0
  76. package/dist/git/__tests__/credentials.test.js +128 -0
  77. package/dist/git/auto-commit.d.ts +3 -0
  78. package/dist/git/auto-commit.js +209 -0
  79. package/dist/git/credentials.d.ts +15 -0
  80. package/dist/git/credentials.js +65 -0
  81. package/dist/index.d.ts +1 -0
  82. package/dist/index.js +31 -0
  83. package/dist/logs/tailer.d.ts +13 -0
  84. package/dist/logs/tailer.js +135 -0
  85. package/dist/template/manifest.d.ts +17 -0
  86. package/dist/template/manifest.js +102 -0
  87. package/dist/types-manager.d.ts +1 -0
  88. package/dist/types-manager.js +35 -0
  89. package/dist/types.d.ts +27 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/__tests__/prompt.test.d.ts +1 -0
  92. package/dist/utils/__tests__/prompt.test.js +103 -0
  93. package/dist/utils/fs.d.ts +5 -0
  94. package/dist/utils/fs.js +23 -0
  95. package/dist/utils/prompt.d.ts +4 -0
  96. package/dist/utils/prompt.js +34 -0
  97. package/dist/utils/zip.d.ts +1 -0
  98. package/dist/utils/zip.js +17 -0
  99. package/package.json +66 -0
@@ -0,0 +1,102 @@
1
+ import { createHash } from 'crypto';
2
+ import { execFileSync } from 'child_process';
3
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
4
+ import { join, relative } from 'path';
5
+ const SKIP_DIRS = new Set(['.git', 'node_modules', '.runwork']);
6
+ const SKIP_FILES = new Set(['.dev.vars', '.env']);
7
+ function sha256(data) {
8
+ return 'sha256:' + createHash('sha256').update(data).digest('hex');
9
+ }
10
+ function walkDir(dir, base) {
11
+ const results = [];
12
+ const entries = readdirSync(dir, { withFileTypes: true });
13
+ for (const entry of entries) {
14
+ if (SKIP_DIRS.has(entry.name))
15
+ continue;
16
+ if (SKIP_FILES.has(entry.name))
17
+ continue;
18
+ const fullPath = join(dir, entry.name);
19
+ if (entry.isDirectory()) {
20
+ results.push(...walkDir(fullPath, base));
21
+ }
22
+ else if (entry.isFile()) {
23
+ results.push(relative(base, fullPath));
24
+ }
25
+ }
26
+ return results;
27
+ }
28
+ export async function generateManifest(dir) {
29
+ const files = {};
30
+ const allFiles = walkDir(dir, dir);
31
+ for (const relPath of allFiles) {
32
+ const content = readFileSync(join(dir, relPath));
33
+ files[relPath] = sha256(content);
34
+ }
35
+ return { version: 1, files };
36
+ }
37
+ export async function saveManifest(dir, manifest) {
38
+ const manifestDir = join(dir, '.runwork');
39
+ if (!existsSync(manifestDir)) {
40
+ mkdirSync(manifestDir, { recursive: true });
41
+ }
42
+ writeFileSync(join(manifestDir, 'template-manifest.json'), JSON.stringify(manifest, null, 2));
43
+ }
44
+ export async function loadManifest(dir) {
45
+ const manifestPath = join(dir, '.runwork', 'template-manifest.json');
46
+ if (!existsSync(manifestPath))
47
+ return null;
48
+ try {
49
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Detect files the user edited outside of `runwork dev`.
57
+ *
58
+ * Two sources of edits:
59
+ * 1. Git-tracked files with uncommitted changes (git diff + git diff --cached)
60
+ * 2. Untracked files that aren't in the template manifest (user-created files)
61
+ *
62
+ * Already-committed files are NOT flagged -- git already knows about them.
63
+ */
64
+ export async function detectUserEdits(dir, manifest) {
65
+ const edits = [];
66
+ // 1. Uncommitted changes to tracked files
67
+ try {
68
+ const modified = execFileSync('git', ['diff', '--name-only', 'HEAD'], { cwd: dir, encoding: 'utf-8' }).trim();
69
+ const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: dir, encoding: 'utf-8' }).trim();
70
+ for (const file of [...modified.split('\n'), ...staged.split('\n')]) {
71
+ if (file && !edits.includes(file))
72
+ edits.push(file);
73
+ }
74
+ }
75
+ catch {
76
+ // No git repo or no commits yet
77
+ }
78
+ // 2. Untracked files that aren't template files
79
+ try {
80
+ const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd: dir, encoding: 'utf-8' }).trim();
81
+ for (const relPath of untracked.split('\n')) {
82
+ if (!relPath)
83
+ continue;
84
+ // Skip files in ignored directories
85
+ if (relPath.startsWith('node_modules/') || relPath.startsWith('.runwork/'))
86
+ continue;
87
+ // If it's a pristine template file, skip it
88
+ const expectedHash = manifest.files[relPath];
89
+ if (expectedHash) {
90
+ const content = readFileSync(join(dir, relPath));
91
+ if (sha256(content) === expectedHash)
92
+ continue;
93
+ }
94
+ if (!edits.includes(relPath))
95
+ edits.push(relPath);
96
+ }
97
+ }
98
+ catch {
99
+ // No git repo
100
+ }
101
+ return edits;
102
+ }
@@ -0,0 +1 @@
1
+ export declare function populateTypes(projectDir: string): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import { existsSync, mkdirSync, readdirSync, copyFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { BUNDLED_TYPES } from './generated/bundled-types.js';
4
+ const TYPES_DIR = '.runwork/types';
5
+ export async function populateTypes(projectDir) {
6
+ const typesDir = join(projectDir, TYPES_DIR);
7
+ mkdirSync(typesDir, { recursive: true });
8
+ // Tier 1: Copy from node_modules/@runworkai/framework/dist/
9
+ const frameworkDist = join(projectDir, 'node_modules/@runworkai/framework/dist');
10
+ if (existsSync(frameworkDist)) {
11
+ copyDtsFiles(frameworkDist, typesDir);
12
+ console.log('Types populated from node_modules/@runworkai/framework');
13
+ return;
14
+ }
15
+ // Tier 2: Download from platform (TODO - stub for now)
16
+ // Will be implemented when platform has a types download endpoint
17
+ // Tier 3: Write from embedded bundled types
18
+ const typeEntries = Object.entries(BUNDLED_TYPES);
19
+ if (typeEntries.length > 0) {
20
+ for (const [filename, content] of typeEntries) {
21
+ writeFileSync(join(typesDir, filename), content);
22
+ }
23
+ console.log('Types populated from bundled types');
24
+ return;
25
+ }
26
+ console.warn('No type definitions found. Install @runworkai/framework or reinstall the CLI.');
27
+ }
28
+ function copyDtsFiles(srcDir, destDir) {
29
+ const files = readdirSync(srcDir);
30
+ for (const file of files) {
31
+ if (file.endsWith('.d.ts')) {
32
+ copyFileSync(join(srcDir, file), join(destDir, file));
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,27 @@
1
+ export interface RunworkConfig {
2
+ workspaceId: string;
3
+ appId: string;
4
+ appName: string;
5
+ }
6
+ export interface Credentials {
7
+ apiKey: string;
8
+ email: string;
9
+ baseUrl: string;
10
+ }
11
+ export interface DevSession {
12
+ sessionId: string;
13
+ previewUrl: string;
14
+ appId: string;
15
+ }
16
+ export interface AppInfo {
17
+ id: string;
18
+ name: string;
19
+ slug: string;
20
+ workspaceId: string;
21
+ workspaceName: string;
22
+ gitRemoteUrl: string;
23
+ }
24
+ export interface WorkspaceInfo {
25
+ id: string;
26
+ name: string;
27
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ const mockQuestion = vi.fn();
3
+ const mockClose = vi.fn();
4
+ vi.mock('readline', () => ({
5
+ createInterface: () => ({
6
+ question: mockQuestion,
7
+ close: mockClose,
8
+ }),
9
+ }));
10
+ const { promptSelect, promptInput } = await import('../prompt.js');
11
+ describe('utils/prompt', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ describe('promptSelect()', () => {
16
+ it('throws on empty choices array', async () => {
17
+ await expect(promptSelect('Pick one', [])).rejects.toThrow('No choices available');
18
+ });
19
+ it('auto-selects when only one choice', async () => {
20
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
21
+ const result = await promptSelect('Pick one', [
22
+ { label: 'Only Option', value: 42 },
23
+ ]);
24
+ expect(result).toEqual({ label: 'Only Option', value: 42 });
25
+ expect(logSpy).toHaveBeenCalledWith('Pick one: Only Option');
26
+ logSpy.mockRestore();
27
+ });
28
+ it('auto-selects preserves all properties on the choice object', async () => {
29
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
30
+ const choice = { label: 'Test', id: 'abc', extra: true };
31
+ const result = await promptSelect('Select', [choice]);
32
+ expect(result).toBe(choice);
33
+ logSpy.mockRestore();
34
+ });
35
+ it('returns correct choice by index when multiple choices', async () => {
36
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
37
+ // promptSelect calls promptInput internally, which uses readline
38
+ // Mock the question callback to simulate user selecting "2"
39
+ mockQuestion.mockImplementation((_prompt, cb) => {
40
+ cb('2');
41
+ });
42
+ const choices = [
43
+ { label: 'First', id: 1 },
44
+ { label: 'Second', id: 2 },
45
+ { label: 'Third', id: 3 },
46
+ ];
47
+ const result = await promptSelect('Choose', choices);
48
+ expect(result).toEqual({ label: 'Second', id: 2 });
49
+ logSpy.mockRestore();
50
+ });
51
+ it('exits process on invalid selection', async () => {
52
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
53
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
54
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
55
+ throw new Error('process.exit');
56
+ }));
57
+ mockQuestion.mockImplementation((_prompt, cb) => {
58
+ cb('99');
59
+ });
60
+ const choices = [
61
+ { label: 'First', id: 1 },
62
+ { label: 'Second', id: 2 },
63
+ ];
64
+ await expect(promptSelect('Choose', choices)).rejects.toThrow('process.exit');
65
+ expect(exitSpy).toHaveBeenCalledWith(1);
66
+ logSpy.mockRestore();
67
+ errorSpy.mockRestore();
68
+ exitSpy.mockRestore();
69
+ });
70
+ });
71
+ describe('promptInput()', () => {
72
+ it('returns user input trimmed', async () => {
73
+ mockQuestion.mockImplementation((_prompt, cb) => {
74
+ cb(' user input ');
75
+ });
76
+ const result = await promptInput('Enter value');
77
+ expect(result).toBe('user input');
78
+ expect(mockClose).toHaveBeenCalled();
79
+ });
80
+ it('returns default value when input is empty', async () => {
81
+ mockQuestion.mockImplementation((_prompt, cb) => {
82
+ cb('');
83
+ });
84
+ const result = await promptInput('Enter value', 'default-val');
85
+ expect(result).toBe('default-val');
86
+ });
87
+ it('includes default value hint in prompt', async () => {
88
+ mockQuestion.mockImplementation((_prompt, cb) => {
89
+ cb('answer');
90
+ });
91
+ await promptInput('Name', 'default');
92
+ const promptText = mockQuestion.mock.calls[0][0];
93
+ expect(promptText).toContain('[default]');
94
+ });
95
+ it('returns empty string when no input and no default', async () => {
96
+ mockQuestion.mockImplementation((_prompt, cb) => {
97
+ cb(' ');
98
+ });
99
+ const result = await promptInput('Enter');
100
+ expect(result).toBe('');
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Remove .git directories nested at depth >= 2 under the given root.
3
+ * Replaces `find <dir> -mindepth 2 -name .git -type d`.
4
+ */
5
+ export declare function removeNestedGitDirs(rootDir: string): void;
@@ -0,0 +1,23 @@
1
+ import { readdirSync, rmSync } from 'fs';
2
+ import { join, relative } from 'path';
3
+ /**
4
+ * Remove .git directories nested at depth >= 2 under the given root.
5
+ * Replaces `find <dir> -mindepth 2 -name .git -type d`.
6
+ */
7
+ export function removeNestedGitDirs(rootDir) {
8
+ const entries = readdirSync(rootDir, { recursive: true, withFileTypes: true });
9
+ for (const entry of entries) {
10
+ if (entry.name !== '.git' || !entry.isDirectory())
11
+ continue;
12
+ const parentPath = 'parentPath' in entry
13
+ ? entry.parentPath
14
+ : entry.path;
15
+ const fullPath = join(parentPath, entry.name);
16
+ const rel = relative(rootDir, fullPath);
17
+ const depth = rel.split('/').length;
18
+ // Only remove .git dirs at depth >= 2 (not the root .git)
19
+ if (depth >= 2) {
20
+ rmSync(fullPath, { recursive: true, force: true });
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ export declare function promptSelect<T extends {
2
+ label: string;
3
+ }>(message: string, choices: T[]): Promise<T>;
4
+ export declare function promptInput(message: string, defaultValue?: string): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { createInterface } from 'readline';
2
+ export async function promptSelect(message, choices) {
3
+ if (choices.length === 0) {
4
+ throw new Error('No choices available');
5
+ }
6
+ if (choices.length === 1) {
7
+ console.log(`${message}: ${choices[0].label}`);
8
+ return choices[0];
9
+ }
10
+ console.log(message);
11
+ for (let i = 0; i < choices.length; i++) {
12
+ console.log(` ${i + 1}. ${choices[i].label}`);
13
+ }
14
+ const answer = await promptInput(`Select (1-${choices.length})`, '1');
15
+ const index = parseInt(answer, 10) - 1;
16
+ if (isNaN(index) || index < 0 || index >= choices.length) {
17
+ console.error('Invalid selection.');
18
+ process.exit(1);
19
+ }
20
+ return choices[index];
21
+ }
22
+ export async function promptInput(message, defaultValue) {
23
+ const rl = createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout,
26
+ });
27
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
28
+ return new Promise((resolve) => {
29
+ rl.question(`${message}${suffix}: `, (answer) => {
30
+ rl.close();
31
+ resolve(answer.trim() || defaultValue || '');
32
+ });
33
+ });
34
+ }
@@ -0,0 +1 @@
1
+ export declare function extractZip(zipData: ArrayBuffer, targetDir: string): void;
@@ -0,0 +1,17 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { unzipSync } from 'fflate';
4
+ export function extractZip(zipData, targetDir) {
5
+ const files = unzipSync(new Uint8Array(zipData));
6
+ for (const [path, data] of Object.entries(files)) {
7
+ const fullPath = join(targetDir, path);
8
+ // Directory entries end with /
9
+ if (path.endsWith('/')) {
10
+ mkdirSync(fullPath, { recursive: true });
11
+ continue;
12
+ }
13
+ // Ensure parent directory exists
14
+ mkdirSync(dirname(fullPath), { recursive: true });
15
+ writeFileSync(fullPath, data);
16
+ }
17
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "runwork",
3
+ "version": "0.2.0",
4
+ "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
+ "license": "MIT",
6
+ "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",
7
+ "homepage": "https://www.runwork.ai",
8
+ "keywords": [
9
+ "runwork",
10
+ "runworkai",
11
+ "cli",
12
+ "developer-tools",
13
+ "local-development",
14
+ "deploy",
15
+ "preview",
16
+ "full-stack",
17
+ "typescript",
18
+ "ai",
19
+ "ai-agents",
20
+ "vibe",
21
+ "vibe-coding",
22
+ "edge",
23
+ "serverless"
24
+ ],
25
+ "type": "module",
26
+ "bin": {
27
+ "runwork": "./bin/runwork.js"
28
+ },
29
+ "scripts": {
30
+ "prebuild": "mkdir -p bundled-types && cp ../framework/dist/*.d.ts bundled-types/ 2>/dev/null || true && bun run scripts/embed-types.ts",
31
+ "build": "bun run prebuild && tsc",
32
+ "build:binary": "bun run prebuild && bun build src/index.ts --compile --outfile dist/runwork",
33
+ "build:binaries": "bun run scripts/build-binaries.ts",
34
+ "dev": "tsc --watch",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "vitest run",
37
+ "prepublishOnly": "bun run prebuild && tsc",
38
+ "release": "npm publish && npm dist-tag add runwork@$(node -p \"require('./package.json').version\") next",
39
+ "release:binaries": "bun run scripts/release-binaries.ts",
40
+ "release:binaries:upload": "bun run scripts/release-binaries.ts --skip-build",
41
+ "release:full": "bun run release && bun run scripts/release-binaries.ts"
42
+ },
43
+ "dependencies": {
44
+ "chokidar": "^4.0.0",
45
+ "commander": "^13.0.0",
46
+ "fflate": "^0.8.2",
47
+ "open": "^10.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^5.7.0",
51
+ "@types/node": "^22.0.0",
52
+ "vitest": "^3.2.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "files": [
58
+ "dist/**/*.js",
59
+ "dist/**/*.d.ts",
60
+ "dist/**/*.json",
61
+ "bin/",
62
+ "bundled-types/",
63
+ "LICENSE",
64
+ "README.md"
65
+ ]
66
+ }