runwork 0.14.0 → 0.15.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.
- package/dist/__tests__/install-scripts.test.js +33 -0
- package/dist/agents/__tests__/chatgpt-registry.test.d.ts +1 -0
- package/dist/agents/__tests__/chatgpt-registry.test.js +61 -0
- package/dist/agents/__tests__/detection.test.js +3 -0
- package/dist/agents/detection.js +3 -0
- package/dist/agents/intro-skill.js +5 -3
- package/dist/agents/registry-data.d.ts +59 -2
- package/dist/agents/registry-data.js +176 -0
- package/dist/commands/__tests__/clone-args.test.js +8 -1
- package/dist/commands/__tests__/clone-source-result.test.d.ts +1 -0
- package/dist/commands/__tests__/clone-source-result.test.js +45 -0
- package/dist/commands/clone.d.ts +33 -0
- package/dist/commands/clone.js +105 -10
- package/dist/commands/deploy.js +128 -11
- package/dist/commands/dev.js +72 -32
- package/dist/commands/doctor.js +64 -3
- package/dist/commands/info.d.ts +3 -0
- package/dist/commands/info.js +11 -0
- package/dist/commands/sync.js +14 -1
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +89 -0
- package/dist/deploy/__tests__/deploy-state.test.d.ts +1 -0
- package/dist/deploy/__tests__/deploy-state.test.js +80 -0
- package/dist/deploy/__tests__/deploy-status.test.d.ts +1 -0
- package/dist/deploy/__tests__/deploy-status.test.js +41 -0
- package/dist/deploy/__tests__/detach-args.test.d.ts +1 -0
- package/dist/deploy/__tests__/detach-args.test.js +30 -0
- package/dist/deploy/deploy-state.d.ts +36 -0
- package/dist/deploy/deploy-state.js +63 -0
- package/dist/deploy/deploy-status.d.ts +21 -0
- package/dist/deploy/deploy-status.js +36 -0
- package/dist/deploy/detach.d.ts +32 -0
- package/dist/deploy/detach.js +71 -0
- package/dist/dev/__tests__/session.test.js +39 -0
- package/dist/dev/__tests__/startup-sync.test.d.ts +1 -0
- package/dist/dev/__tests__/startup-sync.test.js +32 -0
- package/dist/dev/session.d.ts +9 -0
- package/dist/dev/session.js +4 -1
- package/dist/dev/startup-sync.d.ts +23 -0
- package/dist/dev/startup-sync.js +14 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/git/__tests__/classify-sync-error.test.d.ts +1 -0
- package/dist/git/__tests__/classify-sync-error.test.js +57 -0
- package/dist/git/__tests__/ensure-git-credential-helper.test.d.ts +1 -0
- package/dist/git/__tests__/ensure-git-credential-helper.test.js +133 -0
- package/dist/git/__tests__/ensure-runwork-remote.test.d.ts +1 -0
- package/dist/git/__tests__/ensure-runwork-remote.test.js +109 -0
- package/dist/git/__tests__/repo-config.test.d.ts +1 -0
- package/dist/git/__tests__/repo-config.test.js +26 -0
- package/dist/git/classify-sync-error.d.ts +21 -0
- package/dist/git/classify-sync-error.js +111 -0
- package/dist/git/credentials.d.ts +43 -0
- package/dist/git/credentials.js +67 -0
- package/dist/git/remote.d.ts +37 -0
- package/dist/git/remote.js +75 -0
- package/dist/git/repo-config.d.ts +8 -0
- package/dist/git/repo-config.js +29 -0
- package/dist/health/__tests__/checks.test.js +2 -2
- package/dist/health/__tests__/fix.test.d.ts +1 -0
- package/dist/health/__tests__/fix.test.js +72 -0
- package/dist/health/__tests__/runner-filter.test.d.ts +1 -0
- package/dist/health/__tests__/runner-filter.test.js +61 -0
- package/dist/health/checks.d.ts +7 -0
- package/dist/health/checks.js +50 -36
- package/dist/health/fix.d.ts +22 -0
- package/dist/health/fix.js +62 -0
- package/dist/health/runner.d.ts +7 -1
- package/dist/health/runner.js +40 -27
- package/dist/index.js +2 -0
- package/dist/tools/types.d.ts +5 -0
- package/dist/utils/__tests__/help-json.test.d.ts +1 -0
- package/dist/utils/__tests__/help-json.test.js +39 -0
- package/dist/utils/__tests__/ignore-matcher.test.js +18 -1
- package/dist/utils/help-json.d.ts +14 -0
- package/dist/utils/help-json.js +4 -1
- package/dist/validate/__tests__/validate.test.d.ts +1 -0
- package/dist/validate/__tests__/validate.test.js +42 -0
- package/dist/validate/freshness.d.ts +28 -0
- package/dist/validate/freshness.js +48 -0
- package/dist/validate/preview.d.ts +21 -0
- package/dist/validate/preview.js +39 -0
- package/package.json +1 -1
package/dist/health/checks.js
CHANGED
|
@@ -8,6 +8,8 @@ import { ApiClient } from '../api/client.js';
|
|
|
8
8
|
import { detectAgents, getAdapterBySlug } from '../agents/detect.js';
|
|
9
9
|
import { httpFetch } from '../utils/http.js';
|
|
10
10
|
import { probeGit, parseGitVersion } from '../git/preflight.js';
|
|
11
|
+
import { lookupCredentialHelper, helperBinaryStatus } from '../git/credentials.js';
|
|
12
|
+
import { getDeploySummary } from '../deploy/deploy-state.js';
|
|
11
13
|
// First-party distribution endpoints. Defaults match the plan, can be
|
|
12
14
|
// overridden via RUNWORK_DOWNLOAD_BASE_URL for staging or debugging.
|
|
13
15
|
const BASE_URL = process.env.RUNWORK_DOWNLOAD_BASE_URL || 'https://runwork.ai';
|
|
@@ -283,56 +285,40 @@ export async function checkGitCredentialHelper(ctx) {
|
|
|
283
285
|
fix: 'runwork login',
|
|
284
286
|
};
|
|
285
287
|
}
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
helperConfig = buf.toString('utf-8');
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
288
|
+
// The scoped lookup (get-regexp + origin match) is shared with the
|
|
289
|
+
// lazy-ensure path in git/credentials.ts so the two can't drift. We only
|
|
290
|
+
// need to map its result onto doctor's richer status messages here.
|
|
291
|
+
const lookup = lookupCredentialHelper(origin);
|
|
292
|
+
if (lookup.status === 'none') {
|
|
294
293
|
return {
|
|
295
294
|
name: 'git-credential-helper',
|
|
296
295
|
status: 'fail',
|
|
297
296
|
message: `no credential helper registered for ${origin}`,
|
|
298
|
-
fix: 'runwork
|
|
297
|
+
fix: 'runwork doctor --fix',
|
|
299
298
|
};
|
|
300
299
|
}
|
|
301
|
-
|
|
302
|
-
// <scope> matches the user's actual baseUrl origin (not e.g. a stale
|
|
303
|
-
// staging origin from a previous login).
|
|
304
|
-
const lines = helperConfig.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
305
|
-
const escapedOrigin = origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
306
|
-
const scopedRegex = new RegExp(`^credential\\.${escapedOrigin}\\.helper\\s+(.+)$`);
|
|
307
|
-
const matchedLine = lines.find(line => scopedRegex.test(line));
|
|
308
|
-
if (!matchedLine) {
|
|
300
|
+
if (lookup.status === 'unscoped') {
|
|
309
301
|
return {
|
|
310
302
|
name: 'git-credential-helper',
|
|
311
303
|
status: 'fail',
|
|
312
|
-
message: `no credential helper registered for ${origin} (found ${
|
|
313
|
-
fix: 'runwork
|
|
304
|
+
message: `no credential helper registered for ${origin} (found ${lookup.otherCount} other runwork helper entries)`,
|
|
305
|
+
fix: 'runwork doctor --fix',
|
|
314
306
|
};
|
|
315
307
|
}
|
|
316
|
-
const value = scopedRegex.exec(matchedLine)?.[1] ?? '';
|
|
317
308
|
// Helper values starting with `!` are shell commands. Common shapes:
|
|
318
309
|
// `!runwork git-credential-helper` (PATH-relative)
|
|
319
310
|
// `!"/abs/path/to/runwork" git-credential-helper` (post-Pass-2 hardening, POSIX)
|
|
320
311
|
// `!"C:/.../runwork.exe" git-credential-helper` (post-Pass-2 hardening, Windows)
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
status: 'fail',
|
|
332
|
-
message: `helper registered but binary not found at ${helperPath}`,
|
|
333
|
-
fix: 'runwork login (re-registers with the current binary path)',
|
|
334
|
-
};
|
|
335
|
-
}
|
|
312
|
+
// helperBinaryStatus() validates only absolute-path entries on disk; it
|
|
313
|
+
// trusts PATH-relative entries to resolve at git-invocation time.
|
|
314
|
+
const binary = helperBinaryStatus(lookup.value);
|
|
315
|
+
if (!binary.ok && binary.path) {
|
|
316
|
+
return {
|
|
317
|
+
name: 'git-credential-helper',
|
|
318
|
+
status: 'fail',
|
|
319
|
+
message: `helper registered but binary not found at ${binary.path}`,
|
|
320
|
+
fix: 'runwork doctor --fix (re-registers with the current binary path)',
|
|
321
|
+
};
|
|
336
322
|
}
|
|
337
323
|
return {
|
|
338
324
|
name: 'git-credential-helper',
|
|
@@ -441,9 +427,37 @@ export async function checkGitRemote(ctx) {
|
|
|
441
427
|
name: 'git-remote',
|
|
442
428
|
status: 'fail',
|
|
443
429
|
message: 'remote "runwork" not configured',
|
|
444
|
-
fix: `git remote add runwork ${expectedUrl}`,
|
|
430
|
+
fix: `runwork doctor --fix (or: git remote add runwork ${expectedUrl})`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// ── 7a. Deploy freshness ─────────────────────────────────────────────
|
|
435
|
+
/**
|
|
436
|
+
* Compare the current local HEAD against the last deploy recorded from this
|
|
437
|
+
* machine (`.runwork/last-deploy.json`). Surfaces "you have undeployed
|
|
438
|
+
* commits" so a deploy can be verified at a glance (friction log #8). Silent
|
|
439
|
+
* (skip) outside a project or before the first local deploy.
|
|
440
|
+
*/
|
|
441
|
+
export async function checkDeployFreshness(ctx) {
|
|
442
|
+
if (!ctx.config) {
|
|
443
|
+
return { name: 'deploy-freshness', status: 'skip', message: 'skipped (no project)' };
|
|
444
|
+
}
|
|
445
|
+
const summary = getDeploySummary(ctx.cwd);
|
|
446
|
+
if (!summary.deployedShortSha) {
|
|
447
|
+
return { name: 'deploy-freshness', status: 'skip', message: 'no deploy recorded from this machine' };
|
|
448
|
+
}
|
|
449
|
+
if (summary.inSync === true) {
|
|
450
|
+
return { name: 'deploy-freshness', status: 'pass', message: `local HEAD matches last deploy (${summary.deployedShortSha})` };
|
|
451
|
+
}
|
|
452
|
+
if (summary.inSync === false) {
|
|
453
|
+
return {
|
|
454
|
+
name: 'deploy-freshness',
|
|
455
|
+
status: 'warn',
|
|
456
|
+
message: `local (${summary.localShortSha}) has commits not in the last deploy (${summary.deployedShortSha})`,
|
|
457
|
+
fix: 'runwork deploy',
|
|
445
458
|
};
|
|
446
459
|
}
|
|
460
|
+
return { name: 'deploy-freshness', status: 'skip', message: 'local HEAD unknown' };
|
|
447
461
|
}
|
|
448
462
|
// ── 8. Agent setup ───────────────────────────────────────────────────
|
|
449
463
|
function loadSetupState() {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DoctorContext } from './checks.js';
|
|
2
|
+
/** Check names `runwork doctor --fix` knows how to auto-remediate. */
|
|
3
|
+
export declare const FIXABLE_CHECKS: readonly string[];
|
|
4
|
+
export interface FixOutcome {
|
|
5
|
+
/** The check name this outcome corresponds to. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Whether a remediation was actually applied (false = couldn't, with a reason). */
|
|
8
|
+
applied: boolean;
|
|
9
|
+
/** Human-readable description of what was done, or why it couldn't be. */
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Apply auto-remediations for the failing checks doctor knows how to fix.
|
|
14
|
+
* Only acts on `failingNames` that are in FIXABLE_CHECKS; everything else is
|
|
15
|
+
* left for the user (with the `fix` hint each check already carries).
|
|
16
|
+
*
|
|
17
|
+
* - git-credential-helper -> register the runwork git helper for the logged-in
|
|
18
|
+
* origin (the B1 ensure-path), so headless-auth'd sessions can use git.
|
|
19
|
+
* - git-remote -> point the `runwork` remote at the app's canonical URL (the
|
|
20
|
+
* B2 ensure-path), using the correct `/api/git/...` URL from the API client.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyDoctorFixes(ctx: DoctorContext, failingNames: string[]): Promise<FixOutcome[]>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { ensureGitCredentialHelper } from '../git/credentials.js';
|
|
4
|
+
import { ensureRunworkRemote } from '../git/remote.js';
|
|
5
|
+
/** Check names `runwork doctor --fix` knows how to auto-remediate. */
|
|
6
|
+
export const FIXABLE_CHECKS = ['git-credential-helper', 'git-remote'];
|
|
7
|
+
/**
|
|
8
|
+
* Apply auto-remediations for the failing checks doctor knows how to fix.
|
|
9
|
+
* Only acts on `failingNames` that are in FIXABLE_CHECKS; everything else is
|
|
10
|
+
* left for the user (with the `fix` hint each check already carries).
|
|
11
|
+
*
|
|
12
|
+
* - git-credential-helper -> register the runwork git helper for the logged-in
|
|
13
|
+
* origin (the B1 ensure-path), so headless-auth'd sessions can use git.
|
|
14
|
+
* - git-remote -> point the `runwork` remote at the app's canonical URL (the
|
|
15
|
+
* B2 ensure-path), using the correct `/api/git/...` URL from the API client.
|
|
16
|
+
*/
|
|
17
|
+
export async function applyDoctorFixes(ctx, failingNames) {
|
|
18
|
+
const failing = new Set(failingNames);
|
|
19
|
+
const outcomes = [];
|
|
20
|
+
if (failing.has('git-credential-helper')) {
|
|
21
|
+
if (ctx.credentials?.baseUrl) {
|
|
22
|
+
await ensureGitCredentialHelper(ctx.credentials.baseUrl);
|
|
23
|
+
outcomes.push({
|
|
24
|
+
name: 'git-credential-helper',
|
|
25
|
+
applied: true,
|
|
26
|
+
message: 'registered the runwork git credential helper',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
outcomes.push({
|
|
31
|
+
name: 'git-credential-helper',
|
|
32
|
+
applied: false,
|
|
33
|
+
message: 'not logged in -- run `runwork login` first',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (failing.has('git-remote')) {
|
|
38
|
+
if (!ctx.config || !ctx.client) {
|
|
39
|
+
outcomes.push({
|
|
40
|
+
name: 'git-remote',
|
|
41
|
+
applied: false,
|
|
42
|
+
message: 'no project config -- run inside an app directory',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (!existsSync(join(ctx.cwd, '.git'))) {
|
|
46
|
+
outcomes.push({
|
|
47
|
+
name: 'git-remote',
|
|
48
|
+
applied: false,
|
|
49
|
+
message: 'not a git repository -- run `git init` first',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
ensureRunworkRemote(ctx.cwd, ctx.client.getGitRemoteUrl(ctx.config.workspaceId, ctx.config.appId));
|
|
54
|
+
outcomes.push({
|
|
55
|
+
name: 'git-remote',
|
|
56
|
+
applied: true,
|
|
57
|
+
message: 'configured the `runwork` remote at the canonical URL',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return outcomes;
|
|
62
|
+
}
|
package/dist/health/runner.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { DoctorReport } from './types.js';
|
|
2
|
-
|
|
2
|
+
/** Every check name doctor knows about, in run order. Used to validate `--check`. */
|
|
3
|
+
export declare const ALL_CHECK_NAMES: readonly string[];
|
|
4
|
+
export interface RunChecksOptions {
|
|
5
|
+
/** When set, run only the named checks (skips unrelated work like network). */
|
|
6
|
+
only?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function runAllChecks(options?: RunChecksOptions): Promise<DoctorReport>;
|
package/dist/health/runner.js
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
|
-
import { buildContext, checkSystemDeps, checkCliVersion, checkCliArtifactReachable, checkCliInstallLocation, checkAuthAndNetwork, checkGitCredentialHelper, checkProjectConfig, checkAppExists, checkGitRemote, checkAgentSetup, } from './checks.js';
|
|
2
|
-
|
|
1
|
+
import { buildContext, checkSystemDeps, checkCliVersion, checkCliArtifactReachable, checkCliInstallLocation, checkAuthAndNetwork, checkGitCredentialHelper, checkProjectConfig, checkAppExists, checkGitRemote, checkDeployFreshness, checkAgentSetup, } from './checks.js';
|
|
2
|
+
const CHECK_RUNNERS = [
|
|
3
|
+
{ names: ['system-deps'], run: async () => [await checkSystemDeps()] },
|
|
4
|
+
{ names: ['cli-version'], run: async () => [await checkCliVersion()] },
|
|
5
|
+
{ names: ['cli-artifact'], run: async () => [await checkCliArtifactReachable()] },
|
|
6
|
+
{ names: ['cli-install-location'], run: async () => [await checkCliInstallLocation()] },
|
|
7
|
+
// Auth + Network come from one combined call (network reuses the auth probe).
|
|
8
|
+
{
|
|
9
|
+
names: ['auth', 'network'],
|
|
10
|
+
run: async (ctx) => {
|
|
11
|
+
const { auth, network } = await checkAuthAndNetwork(ctx);
|
|
12
|
+
return [auth, network];
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
// Catches "logged in but git auth not wired up" -- on Windows this surfaces
|
|
16
|
+
// as a Git Credential Manager popup mid-clone with no obvious cause.
|
|
17
|
+
{ names: ['git-credential-helper'], run: async (ctx) => [await checkGitCredentialHelper(ctx)] },
|
|
18
|
+
{ names: ['project-config'], run: async (ctx) => [await checkProjectConfig(ctx)] },
|
|
19
|
+
{ names: ['app-exists'], run: async (ctx) => [await checkAppExists(ctx)] },
|
|
20
|
+
{ names: ['git-remote'], run: async (ctx) => [await checkGitRemote(ctx)] },
|
|
21
|
+
{ names: ['deploy-freshness'], run: async (ctx) => [await checkDeployFreshness(ctx)] },
|
|
22
|
+
{ names: ['agent-setup'], run: async () => [await checkAgentSetup()] },
|
|
23
|
+
];
|
|
24
|
+
/** Every check name doctor knows about, in run order. Used to validate `--check`. */
|
|
25
|
+
export const ALL_CHECK_NAMES = CHECK_RUNNERS.flatMap(r => r.names);
|
|
26
|
+
export async function runAllChecks(options) {
|
|
3
27
|
const ctx = buildContext();
|
|
28
|
+
const only = options?.only && options.only.length ? new Set(options.only) : null;
|
|
4
29
|
const checks = [];
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// up" state that surfaces on Windows as a Git Credential Manager
|
|
19
|
-
// popup mid-clone with no obvious cause.
|
|
20
|
-
checks.push(await checkGitCredentialHelper(ctx));
|
|
21
|
-
// 5. Project config
|
|
22
|
-
checks.push(await checkProjectConfig(ctx));
|
|
23
|
-
// 6. App exists
|
|
24
|
-
checks.push(await checkAppExists(ctx));
|
|
25
|
-
// 7. Git remote
|
|
26
|
-
checks.push(await checkGitRemote(ctx));
|
|
27
|
-
// 8. Agent setup
|
|
28
|
-
checks.push(await checkAgentSetup());
|
|
29
|
-
// Overall status: worst of all checks
|
|
30
|
+
for (const runner of CHECK_RUNNERS) {
|
|
31
|
+
// Skip a runner entirely unless at least one of its names was requested.
|
|
32
|
+
if (only && !runner.names.some(n => only.has(n)))
|
|
33
|
+
continue;
|
|
34
|
+
const results = await runner.run(ctx);
|
|
35
|
+
for (const result of results) {
|
|
36
|
+
// For multi-result runners (auth+network), include only the requested
|
|
37
|
+
// names when scoped -- e.g. `--check auth` shouldn't print network.
|
|
38
|
+
if (only && !only.has(result.name))
|
|
39
|
+
continue;
|
|
40
|
+
checks.push(result);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
30
43
|
const hasFailure = checks.some(c => c.status === 'fail');
|
|
31
44
|
const hasWarning = checks.some(c => c.status === 'warn');
|
|
32
45
|
const status = hasFailure ? 'fail' : hasWarning ? 'warn' : 'pass';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initCommand } from './commands/init.js';
|
|
|
5
5
|
import { cloneCommand } from './commands/clone.js';
|
|
6
6
|
import { devCommand } from './commands/dev.js';
|
|
7
7
|
import { deployCommand } from './commands/deploy.js';
|
|
8
|
+
import { validateCommand } from './commands/validate.js';
|
|
8
9
|
import { logsCommand } from './commands/logs.js';
|
|
9
10
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
10
11
|
import { logoutCommand } from './commands/logout.js';
|
|
@@ -62,6 +63,7 @@ program.addCommand(initCommand);
|
|
|
62
63
|
program.addCommand(cloneCommand);
|
|
63
64
|
program.addCommand(devCommand);
|
|
64
65
|
program.addCommand(deployCommand);
|
|
66
|
+
program.addCommand(validateCommand);
|
|
65
67
|
program.addCommand(logsCommand);
|
|
66
68
|
program.addCommand(upgradeCommand);
|
|
67
69
|
program.addCommand(logoutCommand);
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -16,6 +16,9 @@ export type PlatformString = string | {
|
|
|
16
16
|
* Detection strategy. Supports simple binary/path checks plus richer
|
|
17
17
|
* Windows-specific methods and a nested `any` combinator for tools that
|
|
18
18
|
* ship as both GUI app and CLI binary.
|
|
19
|
+
*
|
|
20
|
+
* `always` means "no local probe; treat as available." Used by web-only
|
|
21
|
+
* agents (e.g. ChatGPT Web) that have no local footprint to detect.
|
|
19
22
|
*/
|
|
20
23
|
export type Detection = {
|
|
21
24
|
method: 'binary' | 'path';
|
|
@@ -26,6 +29,8 @@ export type Detection = {
|
|
|
26
29
|
} | {
|
|
27
30
|
method: 'any';
|
|
28
31
|
target: Detection[];
|
|
32
|
+
} | {
|
|
33
|
+
method: 'always';
|
|
29
34
|
};
|
|
30
35
|
/**
|
|
31
36
|
* Common shape for anything that can be detected and installed locally.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { buildCommandHelpJson } from '../help-json.js';
|
|
4
|
+
function optByFlags(cmd, flags) {
|
|
5
|
+
const json = buildCommandHelpJson(cmd, '1.0.0');
|
|
6
|
+
const opt = json.options.find(o => o.flags === flags);
|
|
7
|
+
if (!opt)
|
|
8
|
+
throw new Error(`option ${flags} not found`);
|
|
9
|
+
return opt;
|
|
10
|
+
}
|
|
11
|
+
describe('help-json option required semantics', () => {
|
|
12
|
+
const cmd = new Command('demo')
|
|
13
|
+
.option('--workspace <id>', 'workspace id (optional; defaults from credentials)')
|
|
14
|
+
.option('--flag', 'a boolean flag')
|
|
15
|
+
.option('--maybe [value]', 'an optional value')
|
|
16
|
+
.requiredOption('--token <t>', 'a genuinely required option');
|
|
17
|
+
it('marks a value-taking-but-optional option as NOT required (the --workspace bug)', () => {
|
|
18
|
+
const o = optByFlags(cmd, '--workspace <id>');
|
|
19
|
+
expect(o.required).toBe(false); // the option itself is not mandatory
|
|
20
|
+
expect(o.valueRequired).toBe(true); // but its value is required when present
|
|
21
|
+
expect(o.optional).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it('marks a boolean flag as neither required nor value-required', () => {
|
|
24
|
+
const o = optByFlags(cmd, '--flag');
|
|
25
|
+
expect(o.required).toBe(false);
|
|
26
|
+
expect(o.valueRequired).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it('marks an optional-value option correctly', () => {
|
|
29
|
+
const o = optByFlags(cmd, '--maybe [value]');
|
|
30
|
+
expect(o.required).toBe(false);
|
|
31
|
+
expect(o.valueRequired).toBe(false);
|
|
32
|
+
expect(o.optional).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it('marks a genuinely mandatory option as required AND value-required', () => {
|
|
35
|
+
const o = optByFlags(cmd, '--token <t>');
|
|
36
|
+
expect(o.required).toBe(true);
|
|
37
|
+
expect(o.valueRequired).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach } from 'vitest';
|
|
|
2
2
|
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
-
import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored, parseGitignoreContent, } from '../ignore-matcher.js';
|
|
5
|
+
import { buildIgnoreSets, defaultIgnoreSets, isPathIgnored, isRelPathIgnored, parseGitignoreContent, } from '../ignore-matcher.js';
|
|
6
6
|
const tempDirs = [];
|
|
7
7
|
function makeTempDir(prefix) {
|
|
8
8
|
const dir = mkdtempSync(join(tmpdir(), `runwork-ignore-${prefix}-`));
|
|
@@ -26,6 +26,23 @@ describe('defaultIgnoreSets()', () => {
|
|
|
26
26
|
expect(sets.dirs.has('.runwork')).toBe(true);
|
|
27
27
|
expect(sets.dirs.has('.bun-cache')).toBe(true);
|
|
28
28
|
});
|
|
29
|
+
it('excludes every .runwork/ state file from deploy/dev auto-commit staging', () => {
|
|
30
|
+
// commitWorkingTree() filters untracked files through isRelPathIgnored,
|
|
31
|
+
// so any path with a .runwork segment must be excluded -- including the
|
|
32
|
+
// local deploy-state/status files written before the detached child
|
|
33
|
+
// commits (Codex review P1).
|
|
34
|
+
const sets = defaultIgnoreSets();
|
|
35
|
+
for (const p of [
|
|
36
|
+
'.runwork/deploy-status.json',
|
|
37
|
+
'.runwork/last-deploy.json',
|
|
38
|
+
'.runwork/deploy-stdout.log',
|
|
39
|
+
'.runwork/dev-session.json',
|
|
40
|
+
'.runwork/setup.json',
|
|
41
|
+
'.runwork/preview-url',
|
|
42
|
+
]) {
|
|
43
|
+
expect(isRelPathIgnored(p, sets), `${p} must be ignored`).toBe(true);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
29
46
|
it('includes .dev.vars and .env file defaults', () => {
|
|
30
47
|
const sets = defaultIgnoreSets();
|
|
31
48
|
expect(sets.files.has('.dev.vars')).toBe(true);
|
|
@@ -11,7 +11,21 @@ import type { Command } from 'commander';
|
|
|
11
11
|
interface JsonOption {
|
|
12
12
|
flags: string;
|
|
13
13
|
description: string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether the OPTION itself must be supplied (Commander's `mandatory`, set
|
|
16
|
+
* via `.makeOptionMandatory()`). This is what an agent reasoning about
|
|
17
|
+
* "do I have to pass this?" wants. Most options are false here even when
|
|
18
|
+
* they take a value -- e.g. `--workspace <id>` is optional (defaults from
|
|
19
|
+
* credentials). Previously this field was wrongly mapped to Commander's
|
|
20
|
+
* `required` (value-required), which marked nearly every option as required.
|
|
21
|
+
*/
|
|
14
22
|
required: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the option's VALUE is required when the flag is present, i.e. it
|
|
25
|
+
* was declared with `<value>` (vs `[value]` or a boolean flag). Independent
|
|
26
|
+
* of whether the option itself must be supplied.
|
|
27
|
+
*/
|
|
28
|
+
valueRequired: boolean;
|
|
15
29
|
optional: boolean;
|
|
16
30
|
variadic: boolean;
|
|
17
31
|
defaultValue?: unknown;
|
package/dist/utils/help-json.js
CHANGED
|
@@ -11,7 +11,10 @@ function serializeOption(opt) {
|
|
|
11
11
|
const result = {
|
|
12
12
|
flags: opt.flags,
|
|
13
13
|
description: opt.description,
|
|
14
|
-
required
|
|
14
|
+
// Commander's `mandatory` = the option must be supplied; `required` = the
|
|
15
|
+
// option's value is required when present. Expose both, correctly named.
|
|
16
|
+
required: opt.mandatory,
|
|
17
|
+
valueRequired: opt.required,
|
|
15
18
|
optional: opt.optional,
|
|
16
19
|
variadic: opt.variadic,
|
|
17
20
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { summarizeFreshness } from '../freshness.js';
|
|
3
|
+
import { detectViteError } from '../preview.js';
|
|
4
|
+
describe('validate/freshness summarizeFreshness()', () => {
|
|
5
|
+
const base = { localSha: 'a', remoteSha: 'a', ahead: 0, behind: 0, uncommitted: false };
|
|
6
|
+
it('is in-sync when local==remote, no ahead/behind, clean tree', () => {
|
|
7
|
+
expect(summarizeFreshness(base).inSync).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it('is not in-sync with uncommitted changes', () => {
|
|
10
|
+
expect(summarizeFreshness({ ...base, uncommitted: true }).inSync).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it('is not in-sync when ahead of the remote', () => {
|
|
13
|
+
expect(summarizeFreshness({ ...base, ahead: 2 }).inSync).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
it('is not in-sync when behind the remote', () => {
|
|
16
|
+
expect(summarizeFreshness({ ...base, behind: 1 }).inSync).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('is not in-sync when there is no remote ref', () => {
|
|
19
|
+
expect(summarizeFreshness({ ...base, remoteSha: null }).inSync).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('is not in-sync when there is no local HEAD', () => {
|
|
22
|
+
expect(summarizeFreshness({ ...base, localSha: null }).inSync).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
it('passes through the raw counts', () => {
|
|
25
|
+
const f = summarizeFreshness({ localSha: 'x', remoteSha: 'y', ahead: 3, behind: 1, uncommitted: true });
|
|
26
|
+
expect(f).toMatchObject({ ahead: 3, behind: 1, uncommitted: true, inSync: false });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('validate/preview detectViteError()', () => {
|
|
30
|
+
it('detects the Vite error overlay element', () => {
|
|
31
|
+
expect(detectViteError('<vite-error-overlay></vite-error-overlay>')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('detects a plugin transform error', () => {
|
|
34
|
+
expect(detectViteError('[plugin:vite:react-babel] Unexpected token')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('detects a failed import resolution', () => {
|
|
37
|
+
expect(detectViteError('Failed to resolve import "./missing" from "src/App.tsx"')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('returns false for a healthy HTML document', () => {
|
|
40
|
+
expect(detectViteError('<!DOCTYPE html><html><body><div id="root"></div></body></html>')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git sync-freshness: how the local working tree relates to the deploy remote
|
|
3
|
+
* (`runwork/main`). Answers "do I have changes that aren't synced/deployed?"
|
|
4
|
+
* without scraping the sandbox (friction log #10).
|
|
5
|
+
*/
|
|
6
|
+
export interface Freshness {
|
|
7
|
+
localSha: string | null;
|
|
8
|
+
remoteSha: string | null;
|
|
9
|
+
ahead: number;
|
|
10
|
+
behind: number;
|
|
11
|
+
uncommitted: boolean;
|
|
12
|
+
/** true when local == remote and the tree is clean. */
|
|
13
|
+
inSync: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FreshnessInput {
|
|
16
|
+
localSha: string | null;
|
|
17
|
+
remoteSha: string | null;
|
|
18
|
+
ahead: number;
|
|
19
|
+
behind: number;
|
|
20
|
+
uncommitted: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Pure: derive the in-sync verdict from the raw git counts/flags. */
|
|
23
|
+
export declare function summarizeFreshness(input: FreshnessInput): Freshness;
|
|
24
|
+
/**
|
|
25
|
+
* Collect git freshness for `cwd`. Best-effort fetches `runwork/main` first so
|
|
26
|
+
* the comparison reflects the real remote tip; tolerates offline/no-remote.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getGitFreshness(cwd: string): Freshness;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFileSync } from '../utils/subprocess.js';
|
|
2
|
+
/** Pure: derive the in-sync verdict from the raw git counts/flags. */
|
|
3
|
+
export function summarizeFreshness(input) {
|
|
4
|
+
const inSync = input.localSha !== null &&
|
|
5
|
+
input.remoteSha !== null &&
|
|
6
|
+
input.ahead === 0 &&
|
|
7
|
+
input.behind === 0 &&
|
|
8
|
+
!input.uncommitted;
|
|
9
|
+
return { ...input, inSync };
|
|
10
|
+
}
|
|
11
|
+
function gitOut(cwd, args) {
|
|
12
|
+
try {
|
|
13
|
+
return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Collect git freshness for `cwd`. Best-effort fetches `runwork/main` first so
|
|
21
|
+
* the comparison reflects the real remote tip; tolerates offline/no-remote.
|
|
22
|
+
*/
|
|
23
|
+
export function getGitFreshness(cwd) {
|
|
24
|
+
// Best-effort: refresh the remote-tracking ref. Failure (offline, no remote)
|
|
25
|
+
// just means we compare against whatever ref we already have.
|
|
26
|
+
try {
|
|
27
|
+
execFileSync('git', ['fetch', 'runwork', 'main'], { cwd, stdio: 'pipe' });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* offline or no runwork remote -- non-fatal */
|
|
31
|
+
}
|
|
32
|
+
const localSha = gitOut(cwd, ['rev-parse', 'HEAD']);
|
|
33
|
+
const remoteSha = gitOut(cwd, ['rev-parse', 'runwork/main']);
|
|
34
|
+
let ahead = 0;
|
|
35
|
+
let behind = 0;
|
|
36
|
+
if (localSha && remoteSha) {
|
|
37
|
+
// `<behind>\t<ahead>` relative to runwork/main...HEAD
|
|
38
|
+
const counts = gitOut(cwd, ['rev-list', '--left-right', '--count', 'runwork/main...HEAD']);
|
|
39
|
+
if (counts) {
|
|
40
|
+
const [b, a] = counts.split(/\s+/).map(n => parseInt(n, 10));
|
|
41
|
+
behind = Number.isFinite(b) ? b : 0;
|
|
42
|
+
ahead = Number.isFinite(a) ? a : 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const porcelain = gitOut(cwd, ['status', '--porcelain']);
|
|
46
|
+
const uncommitted = porcelain !== null && porcelain.length > 0;
|
|
47
|
+
return summarizeFreshness({ localSha, remoteSha, ahead, behind, uncommitted });
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liveness of the preview deployment: does it respond, and is it serving a
|
|
3
|
+
* working build (vs a Vite error overlay)? This is the cheap "is my change
|
|
4
|
+
* live and compiling" signal the friction log (#10) reached for by hand.
|
|
5
|
+
*/
|
|
6
|
+
export interface PreviewLiveness {
|
|
7
|
+
url: string | null;
|
|
8
|
+
responds: boolean;
|
|
9
|
+
status: number | null;
|
|
10
|
+
/** A Vite/build error overlay was detected in the served HTML. */
|
|
11
|
+
errorDetected: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Heuristically detect a Vite dev-server error overlay / transform error in
|
|
16
|
+
* served HTML. These are the markers Vite injects when a module fails to
|
|
17
|
+
* compile, so their presence means the preview is serving a broken build.
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectViteError(html: string): boolean;
|
|
20
|
+
/** Fetch the preview URL and report whether it's live and error-free. */
|
|
21
|
+
export declare function checkPreview(url: string | null): Promise<PreviewLiveness>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { httpFetch } from '../utils/http.js';
|
|
2
|
+
/**
|
|
3
|
+
* Heuristically detect a Vite dev-server error overlay / transform error in
|
|
4
|
+
* served HTML. These are the markers Vite injects when a module fails to
|
|
5
|
+
* compile, so their presence means the preview is serving a broken build.
|
|
6
|
+
*/
|
|
7
|
+
export function detectViteError(html) {
|
|
8
|
+
const markers = [
|
|
9
|
+
'vite-error-overlay',
|
|
10
|
+
'class="vite-error',
|
|
11
|
+
'[vite] Internal Server Error',
|
|
12
|
+
'[plugin:vite:',
|
|
13
|
+
'Pre-transform error',
|
|
14
|
+
'Failed to resolve import',
|
|
15
|
+
'Internal server error:',
|
|
16
|
+
];
|
|
17
|
+
return markers.some(m => html.includes(m));
|
|
18
|
+
}
|
|
19
|
+
/** Fetch the preview URL and report whether it's live and error-free. */
|
|
20
|
+
export async function checkPreview(url) {
|
|
21
|
+
if (!url) {
|
|
22
|
+
return { url: null, responds: false, status: null, errorDetected: false };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const res = await httpFetch(url, { method: 'GET' });
|
|
26
|
+
let errorDetected = false;
|
|
27
|
+
try {
|
|
28
|
+
const body = await res.text();
|
|
29
|
+
errorDetected = detectViteError(body);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* body read failed -- treat as no overlay detected */
|
|
33
|
+
}
|
|
34
|
+
return { url, responds: res.status > 0 && res.status < 500, status: res.status, errorDetected };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return { url, responds: false, status: null, errorDetected: false, error: err instanceof Error ? err.message : String(err) };
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED