runwork 0.9.1 → 0.9.3
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.
- package/dist/agents/__tests__/codex-stats.test.js +6 -0
- package/dist/agents/__tests__/detection.test.d.ts +1 -0
- package/dist/agents/__tests__/detection.test.js +86 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +6 -0
- package/dist/agents/detection.d.ts +6 -5
- package/dist/agents/detection.js +28 -26
- package/dist/agents/registry-data.d.ts +5 -31
- package/dist/agents/registry-data.js +3 -3
- package/dist/commands/__tests__/sync-redetect.test.d.ts +1 -0
- package/dist/commands/__tests__/sync-redetect.test.js +96 -0
- package/dist/commands/dev.js +78 -0
- package/dist/commands/setup.js +4 -1
- package/dist/commands/sync.d.ts +26 -0
- package/dist/commands/sync.js +56 -1
- package/dist/devtools/registry-data.d.ts +39 -0
- package/dist/devtools/registry-data.js +39 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/manifest.test.js +69 -0
- package/dist/git/auto-commit.d.ts +5 -0
- package/dist/git/auto-commit.js +15 -14
- package/dist/git/credentials.js +16 -5
- package/dist/template/manifest.js +30 -7
- package/dist/tools/types.d.ts +47 -0
- package/dist/tools/types.js +8 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/__tests__/ignore-matcher.test.d.ts +1 -0
- package/dist/utils/__tests__/ignore-matcher.test.js +188 -0
- package/dist/utils/ignore-matcher.d.ts +38 -0
- package/dist/utils/ignore-matcher.js +116 -0
- package/package.json +1 -1
package/dist/commands/sync.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { requireAuth } from '../auth/store.js';
|
|
6
6
|
import { ApiClient } from '../api/client.js';
|
|
7
|
-
import { getAdapterBySlug } from '../agents/detect.js';
|
|
7
|
+
import { getAdapterBySlug, detectAgents } from '../agents/detect.js';
|
|
8
8
|
import { CodexAdapter } from '../agents/codex.js';
|
|
9
9
|
import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
|
|
10
10
|
import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
|
|
@@ -60,6 +60,49 @@ function readLocalSkills(state) {
|
|
|
60
60
|
}
|
|
61
61
|
return results;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* How long a detection result stays trusted before sync re-runs detection
|
|
65
|
+
* on its own. Detection is cheap on macOS/Linux but pays a PowerShell
|
|
66
|
+
* waterfall on Windows, so we cache for a day. Users who just installed
|
|
67
|
+
* a new agent can shortcut the wait via `--redetect` (or the desktop's
|
|
68
|
+
* "Rescan" button, which forwards the flag).
|
|
69
|
+
*/
|
|
70
|
+
export const REDETECT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether to re-run installation detection for this sync. We always
|
|
73
|
+
* re-detect on explicit user request, on first sync (no timestamp yet), when
|
|
74
|
+
* setup ended up with zero agents (recovery), or after the TTL has lapsed.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldRedetect(state, opts) {
|
|
77
|
+
if (opts.redetect)
|
|
78
|
+
return true;
|
|
79
|
+
if (state.configuredAgents.length === 0)
|
|
80
|
+
return true;
|
|
81
|
+
if (!state.lastDetectedAt)
|
|
82
|
+
return true;
|
|
83
|
+
const last = Date.parse(state.lastDetectedAt);
|
|
84
|
+
if (Number.isNaN(last))
|
|
85
|
+
return true;
|
|
86
|
+
return Date.now() - last > REDETECT_TTL_MS;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect installed agents and merge any new slugs into `state.configuredAgents`.
|
|
90
|
+
* Never removes agents — a transient PATH glitch shouldn't wipe the setup.
|
|
91
|
+
* Returns the slugs that were newly added (for logging).
|
|
92
|
+
*/
|
|
93
|
+
export async function refreshConfiguredAgents(state) {
|
|
94
|
+
const detected = await detectAgents();
|
|
95
|
+
const before = new Set(state.configuredAgents);
|
|
96
|
+
const added = [];
|
|
97
|
+
for (const adapter of detected) {
|
|
98
|
+
if (!before.has(adapter.slug)) {
|
|
99
|
+
state.configuredAgents.push(adapter.slug);
|
|
100
|
+
added.push(adapter.slug);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
state.lastDetectedAt = new Date().toISOString();
|
|
104
|
+
return added;
|
|
105
|
+
}
|
|
63
106
|
export async function syncFromState(state, statePath, credentials, opts) {
|
|
64
107
|
const client = new ApiClient(credentials);
|
|
65
108
|
// Refresh workspace name/slug if missing or stale (e.g. setup ran before name was set)
|
|
@@ -77,6 +120,16 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
77
120
|
}
|
|
78
121
|
}
|
|
79
122
|
console.log(`Syncing workspace: ${state.workspaceName || state.workspaceId}`);
|
|
123
|
+
// Refresh the configured-agents list when the cached detection is stale,
|
|
124
|
+
// empty, or the user explicitly asked. This is what catches "I just installed
|
|
125
|
+
// Claude Code on this machine" without needing the user to re-run setup.
|
|
126
|
+
if (shouldRedetect(state, opts)) {
|
|
127
|
+
console.log(' Detecting installed agents...');
|
|
128
|
+
const added = await refreshConfiguredAgents(state);
|
|
129
|
+
if (added.length > 0) {
|
|
130
|
+
console.log(` Detected new agent${added.length > 1 ? 's' : ''}: ${added.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
80
133
|
console.log(' Fetching workspace data...');
|
|
81
134
|
// Fetch latest data (includeContent=true gets all skill content in one request)
|
|
82
135
|
const [allSkills, mcpServers, externalSkills, registries, connectedIntegrations] = await Promise.all([
|
|
@@ -436,6 +489,7 @@ export const syncCommand = new Command('sync')
|
|
|
436
489
|
.option('--pull-only', 'Only pull remote changes, never push local edits')
|
|
437
490
|
.option('--prefer <side>', 'Auto-resolve conflicts: "local" or "remote" (implies --yes)')
|
|
438
491
|
.option('-y, --yes', 'Non-interactive mode (default: prefer remote for conflicts)')
|
|
492
|
+
.option('--redetect', 'Force agent-installation re-detection (picks up agents installed since last setup)')
|
|
439
493
|
.action(async (opts) => {
|
|
440
494
|
const credentials = requireAuth();
|
|
441
495
|
const syncOpts = {
|
|
@@ -444,6 +498,7 @@ export const syncCommand = new Command('sync')
|
|
|
444
498
|
yes: !!opts.yes || !!opts.prefer,
|
|
445
499
|
prefer: opts.prefer,
|
|
446
500
|
verbose: !!opts.verbose,
|
|
501
|
+
redetect: !!opts.redetect,
|
|
447
502
|
};
|
|
448
503
|
const projectStatePath = join(process.cwd(), '.runwork', 'setup.json');
|
|
449
504
|
const userStatePath = join(homedir(), '.runwork', 'setup.json');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer tool registry — non-AI prerequisites Runwork users need installed
|
|
3
|
+
* locally (git today, more later if needed).
|
|
4
|
+
*
|
|
5
|
+
* Extends the shared `InstallableTool` base so the desktop can detect and
|
|
6
|
+
* render devtools alongside AI agents using the same icon/detection plumbing.
|
|
7
|
+
*
|
|
8
|
+
* Browser-safe: no node-only imports.
|
|
9
|
+
*/
|
|
10
|
+
import type { InstallableTool } from '../tools/types.js';
|
|
11
|
+
/**
|
|
12
|
+
* How the desktop should attempt to install the tool. Each platform either
|
|
13
|
+
* has an automated install command (run via the platform shell) or falls
|
|
14
|
+
* back to opening the tool's `downloadUrl` in the browser.
|
|
15
|
+
*/
|
|
16
|
+
export type DevToolInstallStrategy = {
|
|
17
|
+
kind: 'shell';
|
|
18
|
+
shell: 'powershell' | 'zsh' | 'bash';
|
|
19
|
+
command: string;
|
|
20
|
+
postInstallNote?: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'open-url';
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
export interface DevToolDefinition extends InstallableTool {
|
|
26
|
+
/** Beginner-friendly explanation shown in the install card. */
|
|
27
|
+
longDescription: string;
|
|
28
|
+
/**
|
|
29
|
+
* Per-platform install strategy. Missing platforms fall back to opening
|
|
30
|
+
* `downloadUrl` in the browser.
|
|
31
|
+
*/
|
|
32
|
+
install: {
|
|
33
|
+
macos?: DevToolInstallStrategy;
|
|
34
|
+
windows?: DevToolInstallStrategy;
|
|
35
|
+
linux?: DevToolInstallStrategy;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function getDevTools(): DevToolDefinition[];
|
|
39
|
+
export declare function getDevTool(slug: string): DevToolDefinition | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer tool registry — non-AI prerequisites Runwork users need installed
|
|
3
|
+
* locally (git today, more later if needed).
|
|
4
|
+
*
|
|
5
|
+
* Extends the shared `InstallableTool` base so the desktop can detect and
|
|
6
|
+
* render devtools alongside AI agents using the same icon/detection plumbing.
|
|
7
|
+
*
|
|
8
|
+
* Browser-safe: no node-only imports.
|
|
9
|
+
*/
|
|
10
|
+
const DEV_TOOL_REGISTRY = [
|
|
11
|
+
{
|
|
12
|
+
slug: 'git',
|
|
13
|
+
name: 'Git',
|
|
14
|
+
description: 'Version control system',
|
|
15
|
+
longDescription: 'Git tracks changes to your code and is required for Runwork commands that sync your work with the platform (init, clone, dev, deploy).',
|
|
16
|
+
detection: { method: 'binary', target: 'git' },
|
|
17
|
+
downloadUrl: 'https://git-scm.com/downloads',
|
|
18
|
+
install: {
|
|
19
|
+
windows: {
|
|
20
|
+
kind: 'shell',
|
|
21
|
+
shell: 'powershell',
|
|
22
|
+
command: 'winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements',
|
|
23
|
+
},
|
|
24
|
+
macos: {
|
|
25
|
+
kind: 'shell',
|
|
26
|
+
shell: 'zsh',
|
|
27
|
+
command: 'xcode-select --install',
|
|
28
|
+
postInstallNote: 'A macOS installer window has opened. Finish the install there, then click Re-detect.',
|
|
29
|
+
},
|
|
30
|
+
linux: { kind: 'open-url', url: 'https://git-scm.com/download/linux' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export function getDevTools() {
|
|
35
|
+
return DEV_TOOL_REGISTRY;
|
|
36
|
+
}
|
|
37
|
+
export function getDevTool(slug) {
|
|
38
|
+
return DEV_TOOL_REGISTRY.find((t) => t.slug === slug);
|
|
39
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.9.
|
|
1
|
+
export declare const VERSION = "0.9.3";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
|
-
export const VERSION = "0.9.
|
|
2
|
+
export const VERSION = "0.9.3";
|
|
@@ -67,6 +67,47 @@ describe('generateManifest()', () => {
|
|
|
67
67
|
expect(Object.keys(manifest.files)).toHaveLength(1);
|
|
68
68
|
expect(manifest.files['a.txt']).toBeDefined();
|
|
69
69
|
});
|
|
70
|
+
it('skips .bun-cache directory by default (regression: hung dev startup)', async () => {
|
|
71
|
+
// Bun's per-project cache used to be walked recursively here. When it
|
|
72
|
+
// grew to ~1 GB / 25k+ files, generateManifest spent minutes hashing
|
|
73
|
+
// every file and starved the rest of `runwork dev`. The default
|
|
74
|
+
// ignore-matcher now skips .bun-cache/ even without a .gitignore.
|
|
75
|
+
const dir = makeTempDir('bun-cache');
|
|
76
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
77
|
+
mkdirSync(join(dir, '.bun-cache'));
|
|
78
|
+
writeFileSync(join(dir, '.bun-cache', 'pkg.tgz'), 'tarball');
|
|
79
|
+
mkdirSync(join(dir, '.bun-cache', 'react@19.0.0'));
|
|
80
|
+
writeFileSync(join(dir, '.bun-cache', 'react@19.0.0', 'index.js'), 'r');
|
|
81
|
+
const manifest = await generateManifest(dir);
|
|
82
|
+
expect(Object.keys(manifest.files)).toEqual(['a.txt']);
|
|
83
|
+
});
|
|
84
|
+
it('honors simple .gitignore directory entries (e.g. dist/, .next/, coverage)', async () => {
|
|
85
|
+
const dir = makeTempDir('gitignored-dirs');
|
|
86
|
+
writeFileSync(join(dir, '.gitignore'), 'dist/\n.next/\ncoverage\n');
|
|
87
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
88
|
+
mkdirSync(join(dir, 'dist'));
|
|
89
|
+
writeFileSync(join(dir, 'dist', 'bundle.js'), 'compiled');
|
|
90
|
+
mkdirSync(join(dir, '.next'));
|
|
91
|
+
writeFileSync(join(dir, '.next', 'build.json'), '{}');
|
|
92
|
+
mkdirSync(join(dir, 'coverage'));
|
|
93
|
+
writeFileSync(join(dir, 'coverage', 'lcov.info'), 'data');
|
|
94
|
+
const manifest = await generateManifest(dir);
|
|
95
|
+
const keys = Object.keys(manifest.files).sort();
|
|
96
|
+
expect(keys).toEqual(['.gitignore', 'a.txt']);
|
|
97
|
+
});
|
|
98
|
+
it('falls through to defaults when .gitignore patterns are too complex', async () => {
|
|
99
|
+
// The walker only honors simple directory/basename entries. Globbed
|
|
100
|
+
// patterns are skipped, so files matching them still appear in the
|
|
101
|
+
// manifest. This is acceptable: the goal is to avoid the runaway
|
|
102
|
+
// cases (huge gitignored caches), not to be a full gitignore matcher.
|
|
103
|
+
const dir = makeTempDir('complex-gitignore');
|
|
104
|
+
writeFileSync(join(dir, '.gitignore'), '*.local\nsrc/generated/\n');
|
|
105
|
+
writeFileSync(join(dir, 'a.txt'), 'hello');
|
|
106
|
+
writeFileSync(join(dir, 'config.local'), 'should-still-appear');
|
|
107
|
+
const manifest = await generateManifest(dir);
|
|
108
|
+
expect(manifest.files['a.txt']).toBeDefined();
|
|
109
|
+
expect(manifest.files['config.local']).toBeDefined();
|
|
110
|
+
});
|
|
70
111
|
it('handles files with spaces in names', async () => {
|
|
71
112
|
const dir = makeTempDir('spaces');
|
|
72
113
|
writeFileSync(join(dir, 'hello world.txt'), 'content');
|
|
@@ -374,4 +415,32 @@ describe('detectUserEdits() edge cases', () => {
|
|
|
374
415
|
const edits = await detectUserEdits(dir, manifest);
|
|
375
416
|
expect(edits).not.toContain('config.txt');
|
|
376
417
|
});
|
|
418
|
+
it('does not flag tracked file whose committed content differs from template manifest', async () => {
|
|
419
|
+
// Reproduces the post-`runwork clone` scenario: the manifest was
|
|
420
|
+
// generated from the pristine template, then the AI-customized
|
|
421
|
+
// version was overlaid via `git fetch` + `git checkout .` and is now
|
|
422
|
+
// committed at HEAD with no local edits. Method 3's scan would
|
|
423
|
+
// false-flag every such file on every clone if it didn't skip
|
|
424
|
+
// tracked files.
|
|
425
|
+
const dir = makeTempDir('clone-overlay');
|
|
426
|
+
initGitRepo(dir);
|
|
427
|
+
// Simulate: AI-customized version is committed at HEAD.
|
|
428
|
+
const overlaidContent = 'AI-customized content';
|
|
429
|
+
writeFileSync(join(dir, 'worker/agents.ts'.replace('/', '-')), 'placeholder');
|
|
430
|
+
const aiFilePath = 'agents.ts';
|
|
431
|
+
writeFileSync(join(dir, aiFilePath), overlaidContent);
|
|
432
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
433
|
+
execFileSync('git', ['commit', '-m', 'overlay'], { cwd: dir });
|
|
434
|
+
// Manifest captured the PRISTINE template hash (different from the
|
|
435
|
+
// overlaid HEAD content). detectUserEdits must not treat this as a
|
|
436
|
+
// user edit.
|
|
437
|
+
const manifest = {
|
|
438
|
+
version: 1,
|
|
439
|
+
files: {
|
|
440
|
+
[aiFilePath]: 'sha256:pristine_template_hash_unrelated_to_disk',
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
const edits = await detectUserEdits(dir, manifest);
|
|
444
|
+
expect(edits).not.toContain(aiFilePath);
|
|
445
|
+
});
|
|
377
446
|
});
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ApiClient } from '../api/client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Basename-level ignore predicate using the static defaults only.
|
|
4
|
+
* Exposed for unit testing of the static defaults; the runtime watcher uses
|
|
5
|
+
* a richer matcher built from `.gitignore` (see watchAndAutoCommit).
|
|
6
|
+
*/
|
|
2
7
|
export declare function isIgnored(filePath: string): boolean;
|
|
3
8
|
export declare function watchAndAutoCommit(directory: string, client: ApiClient, appId: string, callbacks?: {
|
|
4
9
|
onFileChange?: (relPath: string, pendingCount: number) => void;
|
package/dist/git/auto-commit.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
3
|
import { watch } from 'chokidar';
|
|
4
|
-
import {
|
|
4
|
+
import { join, relative } from 'path';
|
|
5
5
|
import { dim, cyan, yellow } from '../ui/colors.js';
|
|
6
|
+
import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored } from '../utils/ignore-matcher.js';
|
|
6
7
|
let watcher = null;
|
|
7
8
|
let fastSyncTimer = null;
|
|
8
9
|
let gitTimer = null;
|
|
@@ -13,9 +14,6 @@ let activeCallbacks;
|
|
|
13
14
|
const pendingFastSync = new Map();
|
|
14
15
|
// Track files the user actually touched during this session (for git)
|
|
15
16
|
const changedFiles = new Set();
|
|
16
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', '.runwork']);
|
|
17
|
-
const SKIP_FILES = new Set(['.dev.vars', '.env']);
|
|
18
|
-
const SKIP_EXTENSIONS = new Set(['.log']);
|
|
19
17
|
// Binary extensions to skip in fast sync (git handles them fine)
|
|
20
18
|
const BINARY_EXTENSIONS = new Set([
|
|
21
19
|
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.avif', '.svg',
|
|
@@ -24,21 +22,24 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
24
22
|
'.zip', '.tar', '.gz', '.br',
|
|
25
23
|
'.pdf', '.wasm',
|
|
26
24
|
]);
|
|
25
|
+
const STATIC_IGNORE_SETS = defaultIgnoreSets();
|
|
26
|
+
/**
|
|
27
|
+
* Basename-level ignore predicate using the static defaults only.
|
|
28
|
+
* Exposed for unit testing of the static defaults; the runtime watcher uses
|
|
29
|
+
* a richer matcher built from `.gitignore` (see watchAndAutoCommit).
|
|
30
|
+
*/
|
|
27
31
|
export function isIgnored(filePath) {
|
|
28
|
-
|
|
29
|
-
if (SKIP_DIRS.has(name))
|
|
30
|
-
return true;
|
|
31
|
-
if (SKIP_FILES.has(name))
|
|
32
|
-
return true;
|
|
33
|
-
const ext = name.lastIndexOf('.') >= 0 ? name.slice(name.lastIndexOf('.')) : '';
|
|
34
|
-
if (SKIP_EXTENSIONS.has(ext))
|
|
35
|
-
return true;
|
|
36
|
-
return false;
|
|
32
|
+
return isPathIgnored(filePath, STATIC_IGNORE_SETS);
|
|
37
33
|
}
|
|
38
34
|
export async function watchAndAutoCommit(directory, client, appId, callbacks) {
|
|
39
35
|
activeCallbacks = callbacks;
|
|
36
|
+
// Build a project-scoped ignore matcher that includes simple patterns from
|
|
37
|
+
// the project's .gitignore in addition to the static defaults. Without
|
|
38
|
+
// this the watcher tries to descend into large gitignored caches such as
|
|
39
|
+
// `.bun-cache/` or `dist/` and bogs the CLI down on startup.
|
|
40
|
+
const ignoreSets = buildIgnoreSets(directory);
|
|
40
41
|
watcher = watch(directory, {
|
|
41
|
-
ignored:
|
|
42
|
+
ignored: (p) => isPathIgnored(p, ignoreSets),
|
|
42
43
|
persistent: true,
|
|
43
44
|
ignoreInitial: true,
|
|
44
45
|
awaitWriteFinish: {
|
package/dist/git/credentials.js
CHANGED
|
@@ -6,11 +6,22 @@ import { getCredentials } from '../auth/store.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export async function configureGitCredentials(remoteUrl) {
|
|
8
8
|
const origin = new URL(remoteUrl).origin;
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
try {
|
|
10
|
+
execFileSync('git', [
|
|
11
|
+
'config', '--global',
|
|
12
|
+
`credential.${origin}.helper`,
|
|
13
|
+
'!runwork git-credential-helper',
|
|
14
|
+
], { stdio: 'pipe' });
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const code = err?.code;
|
|
18
|
+
if (code === 'ENOENT') {
|
|
19
|
+
console.warn('Note: git is not installed. Skipping git credential helper setup.');
|
|
20
|
+
console.warn('Install git before running `runwork init`, `clone`, `dev`, or `deploy`.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
14
25
|
}
|
|
15
26
|
/**
|
|
16
27
|
* Remove the git credential helper configuration for our remote.
|
|
@@ -2,22 +2,26 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join, relative } from 'path';
|
|
5
|
-
|
|
6
|
-
const SKIP_FILES = new Set(['.dev.vars', '.env']);
|
|
5
|
+
import { buildIgnoreSets } from '../utils/ignore-matcher.js';
|
|
7
6
|
function sha256(data) {
|
|
8
7
|
return 'sha256:' + createHash('sha256').update(data).digest('hex');
|
|
9
8
|
}
|
|
10
|
-
function walkDir(dir, base) {
|
|
9
|
+
function walkDir(dir, base, sets) {
|
|
11
10
|
const results = [];
|
|
12
11
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
13
12
|
for (const entry of entries) {
|
|
14
|
-
if (
|
|
13
|
+
if (sets.dirs.has(entry.name))
|
|
15
14
|
continue;
|
|
16
|
-
if (
|
|
15
|
+
if (sets.files.has(entry.name))
|
|
17
16
|
continue;
|
|
17
|
+
if (entry.isFile()) {
|
|
18
|
+
const dotIndex = entry.name.lastIndexOf('.');
|
|
19
|
+
if (dotIndex >= 0 && sets.extensions.has(entry.name.slice(dotIndex)))
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
18
22
|
const fullPath = join(dir, entry.name);
|
|
19
23
|
if (entry.isDirectory()) {
|
|
20
|
-
results.push(...walkDir(fullPath, base));
|
|
24
|
+
results.push(...walkDir(fullPath, base, sets));
|
|
21
25
|
}
|
|
22
26
|
else if (entry.isFile()) {
|
|
23
27
|
results.push(relative(base, fullPath));
|
|
@@ -27,7 +31,8 @@ function walkDir(dir, base) {
|
|
|
27
31
|
}
|
|
28
32
|
export async function generateManifest(dir) {
|
|
29
33
|
const files = {};
|
|
30
|
-
const
|
|
34
|
+
const sets = buildIgnoreSets(dir);
|
|
35
|
+
const allFiles = walkDir(dir, dir, sets);
|
|
31
36
|
for (const relPath of allFiles) {
|
|
32
37
|
const content = readFileSync(join(dir, relPath));
|
|
33
38
|
files[relPath] = sha256(content);
|
|
@@ -104,9 +109,27 @@ export async function detectUserEdits(dir, manifest) {
|
|
|
104
109
|
// 3. Scan manifest files on disk directly — catches gitignored files that
|
|
105
110
|
// git ls-files --exclude-standard would miss. If a template file was added
|
|
106
111
|
// to .gitignore by the user and then modified, only this check detects it.
|
|
112
|
+
//
|
|
113
|
+
// Tracked files are excluded here because their state is fully described
|
|
114
|
+
// by Method 1 (`git diff HEAD`). After `runwork clone`, AI-customized
|
|
115
|
+
// versions of template files are committed at HEAD and would otherwise be
|
|
116
|
+
// false-flagged on every fresh clone.
|
|
117
|
+
const trackedFiles = new Set();
|
|
118
|
+
try {
|
|
119
|
+
const tracked = execFileSync('git', ['-c', 'core.quotePath=false', 'ls-files'], { cwd: dir, encoding: 'utf-8' }).trim();
|
|
120
|
+
for (const relPath of tracked.split('\n')) {
|
|
121
|
+
if (relPath)
|
|
122
|
+
trackedFiles.add(relPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// No git repo — every manifest file remains a candidate for Method 3.
|
|
127
|
+
}
|
|
107
128
|
for (const [relPath, expectedHash] of Object.entries(manifest.files)) {
|
|
108
129
|
if (edits.includes(relPath))
|
|
109
130
|
continue;
|
|
131
|
+
if (trackedFiles.has(relPath))
|
|
132
|
+
continue;
|
|
110
133
|
const filePath = join(dir, relPath);
|
|
111
134
|
try {
|
|
112
135
|
const content = readFileSync(filePath);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for everything Runwork can detect and offer to install
|
|
3
|
+
* locally. Both AI agents and developer prerequisites (git, etc.) extend
|
|
4
|
+
* `InstallableTool` so the desktop can render and detect them uniformly.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no node-only imports.
|
|
7
|
+
*/
|
|
8
|
+
/** Platform-specific string. Plain string = same on all platforms. */
|
|
9
|
+
export type PlatformString = string | {
|
|
10
|
+
default?: string;
|
|
11
|
+
macos?: string;
|
|
12
|
+
windows?: string;
|
|
13
|
+
linux?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Detection strategy. Supports simple binary/path checks plus richer
|
|
17
|
+
* Windows-specific methods and a nested `any` combinator for tools that
|
|
18
|
+
* ship as both GUI app and CLI binary.
|
|
19
|
+
*/
|
|
20
|
+
export type Detection = {
|
|
21
|
+
method: 'binary' | 'path';
|
|
22
|
+
target: PlatformString;
|
|
23
|
+
} | {
|
|
24
|
+
method: 'windows-appx' | 'windows-start-app';
|
|
25
|
+
target: string | string[];
|
|
26
|
+
} | {
|
|
27
|
+
method: 'any';
|
|
28
|
+
target: Detection[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Common shape for anything that can be detected and installed locally.
|
|
32
|
+
* AI agents and developer tools both extend this.
|
|
33
|
+
*/
|
|
34
|
+
export interface InstallableTool {
|
|
35
|
+
/** Stable identifier, kebab-case (e.g. "claude-code", "git"). */
|
|
36
|
+
slug: string;
|
|
37
|
+
/** Display name. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** One-line description shown in lists. */
|
|
40
|
+
description: string;
|
|
41
|
+
/** How to determine whether this tool is installed locally. */
|
|
42
|
+
detection: Detection;
|
|
43
|
+
/** Public install/download URL — universal fallback when auto-install isn't available. */
|
|
44
|
+
downloadUrl?: string;
|
|
45
|
+
/** Logo identifier (matches an entry in the desktop's logo map). Optional. */
|
|
46
|
+
logo?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for everything Runwork can detect and offer to install
|
|
3
|
+
* locally. Both AI agents and developer prerequisites (git, etc.) extend
|
|
4
|
+
* `InstallableTool` so the desktop can render and detect them uniformly.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no node-only imports.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -141,6 +141,12 @@ export interface SetupState {
|
|
|
141
141
|
}>;
|
|
142
142
|
reportedAgentSlugs?: string[];
|
|
143
143
|
lastHealthReportAt?: string;
|
|
144
|
+
/**
|
|
145
|
+
* ISO timestamp of the last agent-installation detection pass. Sync uses
|
|
146
|
+
* this with a TTL to skip the (Windows-expensive) PowerShell probe waterfall
|
|
147
|
+
* on every cycle while still catching newly-installed agents within a day.
|
|
148
|
+
*/
|
|
149
|
+
lastDetectedAt?: string;
|
|
144
150
|
}
|
|
145
151
|
export interface WorkflowInfo {
|
|
146
152
|
name: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|