edsger 0.51.0 → 0.53.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/.claude/settings.local.json +23 -3
- package/.env.local +12 -0
- package/dist/commands/find-smells/index.d.ts +21 -0
- package/dist/commands/find-smells/index.js +65 -0
- package/dist/index.js +29 -0
- package/dist/phases/find-bugs/index.js +7 -92
- package/dist/phases/find-bugs/state.d.ts +10 -35
- package/dist/phases/find-bugs/state.js +12 -120
- package/dist/phases/find-features/index.js +16 -83
- package/dist/phases/find-features/prompts.d.ts +7 -1
- package/dist/phases/find-features/prompts.js +31 -11
- package/dist/phases/find-features/state.d.ts +15 -19
- package/dist/phases/find-features/state.js +17 -89
- package/dist/phases/find-features/types.d.ts +1 -1
- package/dist/phases/find-shared/git.d.ts +24 -0
- package/dist/phases/find-shared/git.js +60 -0
- package/dist/phases/find-shared/mcp.d.ts +33 -0
- package/dist/phases/find-shared/mcp.js +69 -0
- package/dist/phases/find-shared/scan-state.d.ts +33 -0
- package/dist/phases/find-shared/scan-state.js +112 -0
- package/dist/phases/find-smells/index.d.ts +47 -0
- package/dist/phases/find-smells/index.js +278 -0
- package/dist/phases/find-smells/prompts.d.ts +30 -0
- package/dist/phases/find-smells/prompts.js +129 -0
- package/dist/phases/find-smells/state.d.ts +21 -0
- package/dist/phases/find-smells/state.js +17 -0
- package/dist/phases/find-smells/types.d.ts +51 -0
- package/dist/phases/find-smells/types.js +64 -0
- package/dist/phases/pr-execution/context.js +40 -32
- package/dist/phases/pr-splitting/context.js +18 -13
- package/dist/utils/github-repo-info.d.ts +13 -1
- package/dist/utils/github-repo-info.js +32 -6
- package/package.json +1 -1
- package/vitest.config.ts +2 -0
- package/dist/api/__tests__/app-store.test.d.ts +0 -7
- package/dist/api/__tests__/app-store.test.js +0 -60
- package/dist/api/__tests__/intelligence.test.d.ts +0 -11
- package/dist/api/__tests__/intelligence.test.js +0 -315
- package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
- package/dist/api/features/__tests__/feature-utils.test.js +0 -370
- package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
- package/dist/api/features/__tests__/status-updater.test.js +0 -88
- package/dist/api/features/approval-checker.d.ts +0 -20
- package/dist/api/features/approval-checker.js +0 -152
- package/dist/api/features/batch-operations.d.ts +0 -17
- package/dist/api/features/batch-operations.js +0 -100
- package/dist/api/features/feature-utils.d.ts +0 -23
- package/dist/api/features/feature-utils.js +0 -80
- package/dist/api/features/get-feature.d.ts +0 -5
- package/dist/api/features/get-feature.js +0 -21
- package/dist/api/features/index.d.ts +0 -8
- package/dist/api/features/index.js +0 -10
- package/dist/api/features/status-updater.d.ts +0 -41
- package/dist/api/features/status-updater.js +0 -122
- package/dist/api/features/test-cases.d.ts +0 -29
- package/dist/api/features/test-cases.js +0 -110
- package/dist/api/features/update-feature.d.ts +0 -20
- package/dist/api/features/update-feature.js +0 -83
- package/dist/api/features/user-stories.d.ts +0 -21
- package/dist/api/features/user-stories.js +0 -88
- package/dist/commands/agent-workflow/feature-worker.d.ts +0 -14
- package/dist/commands/agent-workflow/feature-worker.js +0 -65
- package/dist/commands/build/__tests__/build.test.d.ts +0 -5
- package/dist/commands/build/__tests__/build.test.js +0 -206
- package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
- package/dist/commands/build/__tests__/detect-project.test.js +0 -160
- package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
- package/dist/commands/build/__tests__/run-build.test.js +0 -433
- package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
- package/dist/commands/intelligence/__tests__/command.test.js +0 -48
- package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
- package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
- package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
- package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
- package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
- package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
- package/dist/commands/workflow/core/feature-filter.d.ts +0 -16
- package/dist/commands/workflow/core/feature-filter.js +0 -47
- package/dist/commands/workflow/feature-coordinator.d.ts +0 -18
- package/dist/commands/workflow/feature-coordinator.js +0 -161
- package/dist/config/__tests__/config.test.d.ts +0 -4
- package/dist/config/__tests__/config.test.js +0 -286
- package/dist/config/__tests__/feature-status.test.d.ts +0 -4
- package/dist/config/__tests__/feature-status.test.js +0 -111
- package/dist/config/feature-status.d.ts +0 -56
- package/dist/config/feature-status.js +0 -130
- package/dist/errors/__tests__/index.test.d.ts +0 -4
- package/dist/errors/__tests__/index.test.js +0 -349
- package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
- package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
- package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
- package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
- package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
- package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
- package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
- package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
- package/dist/phases/feature-analysis/agent.d.ts +0 -13
- package/dist/phases/feature-analysis/agent.js +0 -112
- package/dist/phases/feature-analysis/context.d.ts +0 -24
- package/dist/phases/feature-analysis/context.js +0 -138
- package/dist/phases/feature-analysis/index.d.ts +0 -8
- package/dist/phases/feature-analysis/index.js +0 -199
- package/dist/phases/feature-analysis/outcome.d.ts +0 -40
- package/dist/phases/feature-analysis/outcome.js +0 -280
- package/dist/phases/feature-analysis/prompts.d.ts +0 -10
- package/dist/phases/feature-analysis/prompts.js +0 -212
- package/dist/phases/feature-analysis-verification/agent.d.ts +0 -33
- package/dist/phases/feature-analysis-verification/agent.js +0 -124
- package/dist/phases/feature-analysis-verification/index.d.ts +0 -25
- package/dist/phases/feature-analysis-verification/index.js +0 -92
- package/dist/phases/feature-analysis-verification/prompts.d.ts +0 -10
- package/dist/phases/feature-analysis-verification/prompts.js +0 -100
- package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
- package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
- package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
- package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
- package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
- package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
- package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
- package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
- package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
- package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
- package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
- package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
- package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
- package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
- package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
- package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
- package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
- package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
- package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
- package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
- package/dist/phases/run-sheet/render.d.ts +0 -60
- package/dist/phases/run-sheet/render.js +0 -297
- package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
- package/dist/phases/smoke-test/__tests__/agent.test.js +0 -84
- package/dist/phases/smoke-test/__tests__/github.test.d.ts +0 -9
- package/dist/phases/smoke-test/__tests__/github.test.js +0 -120
- package/dist/phases/smoke-test/__tests__/snapshot.test.d.ts +0 -8
- package/dist/phases/smoke-test/__tests__/snapshot.test.js +0 -93
- package/dist/phases/smoke-test/github.d.ts +0 -54
- package/dist/phases/smoke-test/github.js +0 -101
- package/dist/phases/smoke-test/snapshot.d.ts +0 -27
- package/dist/phases/smoke-test/snapshot.js +0 -157
- package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
- package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
- package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
- package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
- package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
- package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
- package/dist/services/lifecycle-agent/index.d.ts +0 -24
- package/dist/services/lifecycle-agent/index.js +0 -25
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
- package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
- package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
- package/dist/services/lifecycle-agent/transition-rules.js +0 -184
- package/dist/services/lifecycle-agent/types.d.ts +0 -190
- package/dist/services/lifecycle-agent/types.js +0 -12
- package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
- package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
- package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
- package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
- package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
- package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
- package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
- package/dist/types/features.d.ts +0 -35
- package/dist/types/features.js +0 -1
- package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
- package/dist/workspace/__tests__/workspace-manager.test.js +0 -52
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { logError, logInfo } from '../../utils/logger.js';
|
|
2
|
-
import { callMcpEndpoint } from '../mcp-client.js';
|
|
3
|
-
/**
|
|
4
|
-
* Get user stories for a feature
|
|
5
|
-
*/
|
|
6
|
-
export async function getUserStories(featureId, verbose) {
|
|
7
|
-
if (verbose) {
|
|
8
|
-
logInfo(`Fetching user stories for feature: ${featureId}`);
|
|
9
|
-
}
|
|
10
|
-
const result = (await callMcpEndpoint('user_stories/list', {
|
|
11
|
-
feature_id: featureId,
|
|
12
|
-
}));
|
|
13
|
-
return (result.user_stories || []);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Create a new user story for a feature
|
|
17
|
-
*/
|
|
18
|
-
export async function createUserStory(featureId, userStory, verbose) {
|
|
19
|
-
try {
|
|
20
|
-
if (verbose) {
|
|
21
|
-
logInfo(`Creating user story for feature: ${featureId}`);
|
|
22
|
-
}
|
|
23
|
-
await callMcpEndpoint('user_stories/create', {
|
|
24
|
-
feature_id: featureId,
|
|
25
|
-
user_stories: [
|
|
26
|
-
{
|
|
27
|
-
title: userStory.title,
|
|
28
|
-
description: userStory.description,
|
|
29
|
-
status: userStory.status || 'draft',
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
-
if (verbose) {
|
|
34
|
-
logInfo('✅ User story created successfully');
|
|
35
|
-
}
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
40
|
-
logError(`Failed to create user story: ${errorMessage}`);
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Delete a user story
|
|
46
|
-
*/
|
|
47
|
-
export async function deleteUserStory(userStoryId, verbose) {
|
|
48
|
-
try {
|
|
49
|
-
if (verbose) {
|
|
50
|
-
logInfo(`Deleting user story: ${userStoryId}`);
|
|
51
|
-
}
|
|
52
|
-
await callMcpEndpoint('user_stories/delete', {
|
|
53
|
-
user_story_id: userStoryId,
|
|
54
|
-
});
|
|
55
|
-
if (verbose) {
|
|
56
|
-
logInfo('✅ User story deleted successfully');
|
|
57
|
-
}
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
62
|
-
logError(`Failed to delete user story: ${errorMessage}`);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Update user story status
|
|
68
|
-
*/
|
|
69
|
-
export async function updateUserStoryStatus(userStoryId, status, verbose) {
|
|
70
|
-
try {
|
|
71
|
-
if (verbose) {
|
|
72
|
-
logInfo(`Updating user story ${userStoryId} status to: ${status}`);
|
|
73
|
-
}
|
|
74
|
-
await callMcpEndpoint('user_stories/update_status', {
|
|
75
|
-
user_story_id: userStoryId,
|
|
76
|
-
status,
|
|
77
|
-
});
|
|
78
|
-
if (verbose) {
|
|
79
|
-
logInfo('✅ User story status updated successfully');
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
85
|
-
logError(`Failed to update user story status: ${errorMessage}`);
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feature Worker - Child process entry point for processing a single feature
|
|
3
|
-
*
|
|
4
|
-
* This file is spawned by the parent AgentWorkflowProcessor via child_process.fork().
|
|
5
|
-
* Each worker runs in its own process with its own cwd, which:
|
|
6
|
-
* 1. Allows concurrent feature processing (no shared process.chdir())
|
|
7
|
-
* 2. After auto-update, new workers load the latest code from disk
|
|
8
|
-
*
|
|
9
|
-
* Communication with parent via IPC messages:
|
|
10
|
-
* - Parent sends: { type: 'start', featureId, verbose, config }
|
|
11
|
-
* - Worker sends: { type: 'result', success: boolean, featureId }
|
|
12
|
-
* - Worker sends: { type: 'log', level, message } for logging
|
|
13
|
-
*/
|
|
14
|
-
export {};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feature Worker - Child process entry point for processing a single feature
|
|
3
|
-
*
|
|
4
|
-
* This file is spawned by the parent AgentWorkflowProcessor via child_process.fork().
|
|
5
|
-
* Each worker runs in its own process with its own cwd, which:
|
|
6
|
-
* 1. Allows concurrent feature processing (no shared process.chdir())
|
|
7
|
-
* 2. After auto-update, new workers load the latest code from disk
|
|
8
|
-
*
|
|
9
|
-
* Communication with parent via IPC messages:
|
|
10
|
-
* - Parent sends: { type: 'start', featureId, verbose, config }
|
|
11
|
-
* - Worker sends: { type: 'result', success: boolean, featureId }
|
|
12
|
-
* - Worker sends: { type: 'log', level, message } for logging
|
|
13
|
-
*/
|
|
14
|
-
import { evaluatePipelineResults } from '../workflow/core/pipeline-evaluator.js';
|
|
15
|
-
import { runFeatureWorkflow } from '../workflow/feature-coordinator.js';
|
|
16
|
-
function sendMessage(msg) {
|
|
17
|
-
if (process.send) {
|
|
18
|
-
process.send(msg);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function log(level, message) {
|
|
22
|
-
sendMessage({ type: 'log', level, message });
|
|
23
|
-
}
|
|
24
|
-
async function handleStart(msg) {
|
|
25
|
-
const { featureId, verbose, config } = msg;
|
|
26
|
-
try {
|
|
27
|
-
log('info', `Worker started for feature: ${featureId}`);
|
|
28
|
-
const results = await runFeatureWorkflow({ featureId, verbose }, config);
|
|
29
|
-
const allSuccessful = evaluatePipelineResults(results);
|
|
30
|
-
sendMessage({
|
|
31
|
-
type: 'result',
|
|
32
|
-
featureId,
|
|
33
|
-
success: allSuccessful,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
38
|
-
log('error', `Worker error for feature ${featureId}: ${errorMessage}`);
|
|
39
|
-
sendMessage({
|
|
40
|
-
type: 'result',
|
|
41
|
-
featureId,
|
|
42
|
-
success: false,
|
|
43
|
-
error: errorMessage,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Listen for messages from parent
|
|
48
|
-
process.on('message', (msg) => {
|
|
49
|
-
if (msg.type === 'start') {
|
|
50
|
-
void handleStart(msg).finally(() => {
|
|
51
|
-
// Exit worker process after completion
|
|
52
|
-
process.exit(0);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
// Handle uncaught errors
|
|
57
|
-
process.on('uncaughtException', (error) => {
|
|
58
|
-
sendMessage({
|
|
59
|
-
type: 'result',
|
|
60
|
-
featureId: 'unknown',
|
|
61
|
-
success: false,
|
|
62
|
-
error: `Uncaught exception: ${error.message}`,
|
|
63
|
-
});
|
|
64
|
-
process.exit(1);
|
|
65
|
-
});
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the build command's pure/utility functions.
|
|
3
|
-
* Tests project discovery, scheme parsing, plist generation, and API key management.
|
|
4
|
-
*/
|
|
5
|
-
import assert from 'node:assert';
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
10
|
-
import { cleanupApiKeyFile, discoverSchemes, findXcodeProjects, generateExportOptionsPlist, writeApiKeyFile, } from '../index.js';
|
|
11
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
12
|
-
function createTmpDir() {
|
|
13
|
-
const dir = join(tmpdir(), `edsger-build-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
14
|
-
mkdirSync(dir, { recursive: true });
|
|
15
|
-
return dir;
|
|
16
|
-
}
|
|
17
|
-
// ── findXcodeProjects ──────────────────────────────────────────────
|
|
18
|
-
describe('findXcodeProjects', () => {
|
|
19
|
-
let tmpDir;
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
tmpDir = createTmpDir();
|
|
22
|
-
});
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
it('returns empty array for directory with no Xcode projects', () => {
|
|
27
|
-
mkdirSync(join(tmpDir, 'src'), { recursive: true });
|
|
28
|
-
writeFileSync(join(tmpDir, 'src', 'main.swift'), '');
|
|
29
|
-
const results = findXcodeProjects(tmpDir);
|
|
30
|
-
assert.deepStrictEqual(results, []);
|
|
31
|
-
});
|
|
32
|
-
it('finds a single .xcodeproj', () => {
|
|
33
|
-
mkdirSync(join(tmpDir, 'MyApp.xcodeproj'), { recursive: true });
|
|
34
|
-
const results = findXcodeProjects(tmpDir);
|
|
35
|
-
assert.strictEqual(results.length, 1);
|
|
36
|
-
assert.strictEqual(results[0].path, 'MyApp.xcodeproj');
|
|
37
|
-
assert.strictEqual(results[0].type, 'project');
|
|
38
|
-
});
|
|
39
|
-
it('finds a single .xcworkspace', () => {
|
|
40
|
-
mkdirSync(join(tmpDir, 'MyApp.xcworkspace'), { recursive: true });
|
|
41
|
-
const results = findXcodeProjects(tmpDir);
|
|
42
|
-
assert.strictEqual(results.length, 1);
|
|
43
|
-
assert.strictEqual(results[0].path, 'MyApp.xcworkspace');
|
|
44
|
-
assert.strictEqual(results[0].type, 'workspace');
|
|
45
|
-
});
|
|
46
|
-
it('sorts workspaces before projects', () => {
|
|
47
|
-
mkdirSync(join(tmpDir, 'MyApp.xcodeproj'), { recursive: true });
|
|
48
|
-
mkdirSync(join(tmpDir, 'MyApp.xcworkspace'), { recursive: true });
|
|
49
|
-
const results = findXcodeProjects(tmpDir);
|
|
50
|
-
assert.strictEqual(results.length, 2);
|
|
51
|
-
assert.strictEqual(results[0].type, 'workspace');
|
|
52
|
-
assert.strictEqual(results[1].type, 'project');
|
|
53
|
-
});
|
|
54
|
-
it('finds projects in subdirectories', () => {
|
|
55
|
-
mkdirSync(join(tmpDir, 'ios', 'MyApp.xcworkspace'), { recursive: true });
|
|
56
|
-
const results = findXcodeProjects(tmpDir);
|
|
57
|
-
assert.strictEqual(results.length, 1);
|
|
58
|
-
assert.strictEqual(results[0].path, join('ios', 'MyApp.xcworkspace'));
|
|
59
|
-
});
|
|
60
|
-
it('excludes Pods directory', () => {
|
|
61
|
-
mkdirSync(join(tmpDir, 'Pods', 'SomePod.xcodeproj'), { recursive: true });
|
|
62
|
-
mkdirSync(join(tmpDir, 'MyApp.xcodeproj'), { recursive: true });
|
|
63
|
-
const results = findXcodeProjects(tmpDir);
|
|
64
|
-
assert.strictEqual(results.length, 1);
|
|
65
|
-
assert.strictEqual(results[0].path, 'MyApp.xcodeproj');
|
|
66
|
-
});
|
|
67
|
-
it('excludes node_modules directory', () => {
|
|
68
|
-
mkdirSync(join(tmpDir, 'node_modules', 'react-native', 'React.xcodeproj'), {
|
|
69
|
-
recursive: true,
|
|
70
|
-
});
|
|
71
|
-
mkdirSync(join(tmpDir, 'ios', 'MyApp.xcodeproj'), { recursive: true });
|
|
72
|
-
const results = findXcodeProjects(tmpDir);
|
|
73
|
-
assert.strictEqual(results.length, 1);
|
|
74
|
-
assert.strictEqual(results[0].path, join('ios', 'MyApp.xcodeproj'));
|
|
75
|
-
});
|
|
76
|
-
it('excludes DerivedData directory', () => {
|
|
77
|
-
mkdirSync(join(tmpDir, 'DerivedData', 'Build.xcodeproj'), {
|
|
78
|
-
recursive: true,
|
|
79
|
-
});
|
|
80
|
-
const results = findXcodeProjects(tmpDir);
|
|
81
|
-
assert.strictEqual(results.length, 0);
|
|
82
|
-
});
|
|
83
|
-
it('excludes .build directory', () => {
|
|
84
|
-
mkdirSync(join(tmpDir, '.build', 'Something.xcodeproj'), {
|
|
85
|
-
recursive: true,
|
|
86
|
-
});
|
|
87
|
-
const results = findXcodeProjects(tmpDir);
|
|
88
|
-
assert.strictEqual(results.length, 0);
|
|
89
|
-
});
|
|
90
|
-
it('sorts shallower paths first among same type', () => {
|
|
91
|
-
mkdirSync(join(tmpDir, 'deep', 'nested', 'A.xcodeproj'), {
|
|
92
|
-
recursive: true,
|
|
93
|
-
});
|
|
94
|
-
mkdirSync(join(tmpDir, 'B.xcodeproj'), { recursive: true });
|
|
95
|
-
const results = findXcodeProjects(tmpDir);
|
|
96
|
-
assert.strictEqual(results[0].path, 'B.xcodeproj');
|
|
97
|
-
});
|
|
98
|
-
it('does not recurse deeper than 4 levels', () => {
|
|
99
|
-
mkdirSync(join(tmpDir, 'a', 'b', 'c', 'd', 'e', 'Deep.xcodeproj'), {
|
|
100
|
-
recursive: true,
|
|
101
|
-
});
|
|
102
|
-
const results = findXcodeProjects(tmpDir);
|
|
103
|
-
assert.strictEqual(results.length, 0);
|
|
104
|
-
});
|
|
105
|
-
it('finds projects at exactly depth 4', () => {
|
|
106
|
-
mkdirSync(join(tmpDir, 'a', 'b', 'c', 'd', 'Ok.xcodeproj'), {
|
|
107
|
-
recursive: true,
|
|
108
|
-
});
|
|
109
|
-
const results = findXcodeProjects(tmpDir);
|
|
110
|
-
assert.strictEqual(results.length, 1);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
// ── generateExportOptionsPlist ─────────────────────────────────────
|
|
114
|
-
describe('generateExportOptionsPlist', () => {
|
|
115
|
-
it('generates valid plist with app-store method', () => {
|
|
116
|
-
const plist = generateExportOptionsPlist('TEAM123', 'app-store');
|
|
117
|
-
assert.ok(plist.includes('<?xml version="1.0"'));
|
|
118
|
-
assert.ok(plist.includes('<string>app-store</string>'));
|
|
119
|
-
assert.ok(plist.includes('<string>TEAM123</string>'));
|
|
120
|
-
assert.ok(plist.includes('<key>uploadSymbols</key>'));
|
|
121
|
-
assert.ok(plist.includes('<true/>'));
|
|
122
|
-
assert.ok(plist.includes('<string>automatic</string>'));
|
|
123
|
-
});
|
|
124
|
-
it('generates plist with ad-hoc method', () => {
|
|
125
|
-
const plist = generateExportOptionsPlist('ABCD', 'ad-hoc');
|
|
126
|
-
assert.ok(plist.includes('<string>ad-hoc</string>'));
|
|
127
|
-
assert.ok(plist.includes('<string>ABCD</string>'));
|
|
128
|
-
});
|
|
129
|
-
it('generates plist with development method', () => {
|
|
130
|
-
const plist = generateExportOptionsPlist('XYZ', 'development');
|
|
131
|
-
assert.ok(plist.includes('<string>development</string>'));
|
|
132
|
-
});
|
|
133
|
-
it('includes all required keys', () => {
|
|
134
|
-
const plist = generateExportOptionsPlist('T', 'app-store');
|
|
135
|
-
assert.ok(plist.includes('<key>method</key>'));
|
|
136
|
-
assert.ok(plist.includes('<key>teamID</key>'));
|
|
137
|
-
assert.ok(plist.includes('<key>uploadSymbols</key>'));
|
|
138
|
-
assert.ok(plist.includes('<key>signingStyle</key>'));
|
|
139
|
-
});
|
|
140
|
-
it('escapes XML special characters in teamId', () => {
|
|
141
|
-
const plist = generateExportOptionsPlist('</string><key>evil</key><string>', 'app-store');
|
|
142
|
-
assert.ok(!plist.includes('<key>evil</key>'));
|
|
143
|
-
assert.ok(plist.includes('</string><key>evil</key><string>'));
|
|
144
|
-
});
|
|
145
|
-
it('escapes XML special characters in exportMethod', () => {
|
|
146
|
-
const plist = generateExportOptionsPlist('TEAM', 'a&b<c>');
|
|
147
|
-
assert.ok(plist.includes('a&b<c>'));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
// ── API key file management ────────────────────────────────────────
|
|
151
|
-
describe('writeApiKeyFile', () => {
|
|
152
|
-
const testKeyId = `test-key-${Date.now()}`;
|
|
153
|
-
const testPrivateKey = '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----';
|
|
154
|
-
afterEach(() => {
|
|
155
|
-
cleanupApiKeyFile(testKeyId);
|
|
156
|
-
});
|
|
157
|
-
it('writes key file with correct content', () => {
|
|
158
|
-
const keyPath = writeApiKeyFile(testKeyId, testPrivateKey);
|
|
159
|
-
assert.ok(existsSync(keyPath));
|
|
160
|
-
assert.strictEqual(readFileSync(keyPath, 'utf-8'), testPrivateKey);
|
|
161
|
-
});
|
|
162
|
-
it('writes key file to ~/.edsger/keys/ directory', () => {
|
|
163
|
-
const keyPath = writeApiKeyFile(testKeyId, testPrivateKey);
|
|
164
|
-
assert.ok(keyPath.includes('.edsger'));
|
|
165
|
-
assert.ok(keyPath.includes('keys'));
|
|
166
|
-
assert.ok(keyPath.endsWith(`.p8`));
|
|
167
|
-
});
|
|
168
|
-
it('names file as AuthKey_<keyId>.p8', () => {
|
|
169
|
-
const keyPath = writeApiKeyFile(testKeyId, testPrivateKey);
|
|
170
|
-
assert.ok(keyPath.endsWith(`AuthKey_${testKeyId}.p8`));
|
|
171
|
-
});
|
|
172
|
-
it('rejects keyId with path traversal characters', () => {
|
|
173
|
-
assert.throws(() => writeApiKeyFile('../../etc/evil', 'key'), (err) => err.message.includes('Invalid API key ID'));
|
|
174
|
-
});
|
|
175
|
-
it('rejects keyId with slashes', () => {
|
|
176
|
-
assert.throws(() => writeApiKeyFile('key/id', 'key'), (err) => err.message.includes('Invalid API key ID'));
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
describe('cleanupApiKeyFile', () => {
|
|
180
|
-
it('removes existing key file', () => {
|
|
181
|
-
const testKeyId = `cleanup-test-${Date.now()}`;
|
|
182
|
-
const keyPath = writeApiKeyFile(testKeyId, 'test');
|
|
183
|
-
assert.ok(existsSync(keyPath));
|
|
184
|
-
cleanupApiKeyFile(testKeyId);
|
|
185
|
-
assert.ok(!existsSync(keyPath));
|
|
186
|
-
});
|
|
187
|
-
it('does not throw for non-existent key', () => {
|
|
188
|
-
assert.doesNotThrow(() => {
|
|
189
|
-
cleanupApiKeyFile('non-existent-key-id');
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
// ── discoverSchemes ────────────────────────────────────────────────
|
|
194
|
-
// Note: These tests only run meaningfully on macOS with Xcode installed.
|
|
195
|
-
// They verify the parsing logic by checking that the function doesn't crash
|
|
196
|
-
// when given a non-existent project (returns empty array).
|
|
197
|
-
describe('discoverSchemes', () => {
|
|
198
|
-
it('returns empty array for non-existent project', () => {
|
|
199
|
-
const schemes = discoverSchemes('/tmp', 'nonexistent.xcodeproj', 'project');
|
|
200
|
-
assert.deepStrictEqual(schemes, []);
|
|
201
|
-
});
|
|
202
|
-
it('returns empty array for non-existent workspace', () => {
|
|
203
|
-
const schemes = discoverSchemes('/tmp', 'nonexistent.xcworkspace', 'workspace');
|
|
204
|
-
assert.deepStrictEqual(schemes, []);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the AI build plan module.
|
|
3
|
-
* Tests the directory tree builder, response parser, and command whitelist
|
|
4
|
-
* without real AI calls.
|
|
5
|
-
*/
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
-
import { tmpdir } from 'node:os';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
11
|
-
import { buildDirectoryTree, parseBuildPlan, validateInstallSteps, } from '../detect-project.js';
|
|
12
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
13
|
-
function createTmpDir() {
|
|
14
|
-
const dir = join(tmpdir(), `edsger-detect-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
15
|
-
mkdirSync(dir, { recursive: true });
|
|
16
|
-
return dir;
|
|
17
|
-
}
|
|
18
|
-
// ── buildDirectoryTree ─────────────────────────────────────────────
|
|
19
|
-
describe('buildDirectoryTree', () => {
|
|
20
|
-
let tmpDir;
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
tmpDir = createTmpDir();
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
it('builds a tree for a React Native structure', () => {
|
|
28
|
-
mkdirSync(join(tmpDir, 'ios', 'MyApp.xcworkspace'), { recursive: true });
|
|
29
|
-
mkdirSync(join(tmpDir, 'ios', 'MyApp.xcodeproj'), { recursive: true });
|
|
30
|
-
mkdirSync(join(tmpDir, 'android'), { recursive: true });
|
|
31
|
-
mkdirSync(join(tmpDir, 'src'), { recursive: true });
|
|
32
|
-
writeFileSync(join(tmpDir, 'package.json'), '{}');
|
|
33
|
-
writeFileSync(join(tmpDir, 'ios', 'Podfile'), '');
|
|
34
|
-
const tree = buildDirectoryTree(tmpDir, 2);
|
|
35
|
-
assert.ok(tree.includes('ios/'));
|
|
36
|
-
assert.ok(tree.includes('MyApp.xcworkspace/'));
|
|
37
|
-
assert.ok(tree.includes('package.json'));
|
|
38
|
-
});
|
|
39
|
-
it('excludes node_modules and Pods', () => {
|
|
40
|
-
mkdirSync(join(tmpDir, 'node_modules', 'pkg'), { recursive: true });
|
|
41
|
-
mkdirSync(join(tmpDir, 'ios', 'Pods'), { recursive: true });
|
|
42
|
-
mkdirSync(join(tmpDir, 'src'), { recursive: true });
|
|
43
|
-
const tree = buildDirectoryTree(tmpDir);
|
|
44
|
-
assert.ok(!tree.includes('node_modules'));
|
|
45
|
-
assert.ok(!tree.includes('Pods'));
|
|
46
|
-
assert.ok(tree.includes('src/'));
|
|
47
|
-
});
|
|
48
|
-
it('respects max depth', () => {
|
|
49
|
-
mkdirSync(join(tmpDir, 'a', 'b', 'c', 'd', 'e'), { recursive: true });
|
|
50
|
-
writeFileSync(join(tmpDir, 'a', 'b', 'c', 'd', 'e', 'deep.txt'), '');
|
|
51
|
-
const tree = buildDirectoryTree(tmpDir, 2);
|
|
52
|
-
assert.ok(!tree.includes('deep.txt'));
|
|
53
|
-
});
|
|
54
|
-
it('returns empty string for empty directory', () => {
|
|
55
|
-
assert.strictEqual(buildDirectoryTree(tmpDir), '');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
// ── parseBuildPlan ─────────────────────────────────────────────────
|
|
59
|
-
describe('parseBuildPlan', () => {
|
|
60
|
-
it('parses a valid JSON response with code block', () => {
|
|
61
|
-
const response = `\`\`\`json
|
|
62
|
-
{
|
|
63
|
-
"build_plan": {
|
|
64
|
-
"project_type": "react-native",
|
|
65
|
-
"install_steps": [
|
|
66
|
-
{ "cmd": "npm", "args": ["ci"], "cwd": "" },
|
|
67
|
-
{ "cmd": "pod", "args": ["install"], "cwd": "ios" }
|
|
68
|
-
],
|
|
69
|
-
"project_path": "ios/MyApp.xcworkspace",
|
|
70
|
-
"scheme_hint": "MyApp",
|
|
71
|
-
"reasoning": "React Native project with CocoaPods"
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
\`\`\``;
|
|
75
|
-
const plan = parseBuildPlan(response);
|
|
76
|
-
assert.ok(plan);
|
|
77
|
-
assert.strictEqual(plan.projectType, 'react-native');
|
|
78
|
-
assert.strictEqual(plan.projectPath, 'ios/MyApp.xcworkspace');
|
|
79
|
-
assert.strictEqual(plan.schemeHint, 'MyApp');
|
|
80
|
-
assert.strictEqual(plan.installSteps.length, 2);
|
|
81
|
-
assert.strictEqual(plan.installSteps[0].cmd, 'npm');
|
|
82
|
-
assert.strictEqual(plan.installSteps[1].cmd, 'pod');
|
|
83
|
-
assert.strictEqual(plan.installSteps[1].cwd, 'ios');
|
|
84
|
-
});
|
|
85
|
-
it('parses raw JSON without code block', () => {
|
|
86
|
-
const response = JSON.stringify({
|
|
87
|
-
build_plan: {
|
|
88
|
-
project_type: 'native',
|
|
89
|
-
install_steps: [],
|
|
90
|
-
project_path: 'App.xcodeproj',
|
|
91
|
-
scheme_hint: 'App',
|
|
92
|
-
reasoning: 'Simple native project',
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
const plan = parseBuildPlan(response);
|
|
96
|
-
assert.ok(plan);
|
|
97
|
-
assert.strictEqual(plan.projectType, 'native');
|
|
98
|
-
assert.strictEqual(plan.installSteps.length, 0);
|
|
99
|
-
});
|
|
100
|
-
it('returns null for empty project_path', () => {
|
|
101
|
-
const response = JSON.stringify({
|
|
102
|
-
build_plan: {
|
|
103
|
-
project_type: 'native',
|
|
104
|
-
install_steps: [],
|
|
105
|
-
project_path: '',
|
|
106
|
-
scheme_hint: '',
|
|
107
|
-
reasoning: '',
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
assert.strictEqual(parseBuildPlan(response), null);
|
|
111
|
-
});
|
|
112
|
-
it('returns null for garbage input', () => {
|
|
113
|
-
assert.strictEqual(parseBuildPlan('not json at all'), null);
|
|
114
|
-
});
|
|
115
|
-
it('handles missing optional fields gracefully', () => {
|
|
116
|
-
const response = JSON.stringify({
|
|
117
|
-
build_plan: {
|
|
118
|
-
project_type: 'flutter',
|
|
119
|
-
project_path: 'ios/Runner.xcworkspace',
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
const plan = parseBuildPlan(response);
|
|
123
|
-
assert.ok(plan);
|
|
124
|
-
assert.strictEqual(plan.schemeHint, '');
|
|
125
|
-
assert.strictEqual(plan.installSteps.length, 0);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
// ── validateInstallSteps ───────────────────────────────────────────
|
|
129
|
-
describe('validateInstallSteps', () => {
|
|
130
|
-
it('allows known safe commands', () => {
|
|
131
|
-
const steps = [
|
|
132
|
-
{ cmd: 'npm', args: ['ci'], cwd: '' },
|
|
133
|
-
{ cmd: 'yarn', args: ['install'], cwd: '' },
|
|
134
|
-
{ cmd: 'pod', args: ['install'], cwd: 'ios' },
|
|
135
|
-
{ cmd: 'flutter', args: ['pub', 'get'], cwd: '' },
|
|
136
|
-
{ cmd: 'xcodegen', args: ['generate'], cwd: '' },
|
|
137
|
-
{ cmd: 'tuist', args: ['generate'], cwd: '' },
|
|
138
|
-
{ cmd: 'bundle', args: ['install'], cwd: '' },
|
|
139
|
-
];
|
|
140
|
-
const valid = validateInstallSteps(steps);
|
|
141
|
-
assert.strictEqual(valid.length, 7);
|
|
142
|
-
});
|
|
143
|
-
it('filters out disallowed commands', () => {
|
|
144
|
-
const steps = [
|
|
145
|
-
{ cmd: 'npm', args: ['ci'], cwd: '' },
|
|
146
|
-
{ cmd: 'rm', args: ['-rf', '/'], cwd: '' },
|
|
147
|
-
{ cmd: 'curl', args: ['http://evil.com'], cwd: '' },
|
|
148
|
-
{ cmd: 'pod', args: ['install'], cwd: 'ios' },
|
|
149
|
-
];
|
|
150
|
-
const valid = validateInstallSteps(steps);
|
|
151
|
-
assert.strictEqual(valid.length, 2);
|
|
152
|
-
assert.strictEqual(valid[0].cmd, 'npm');
|
|
153
|
-
assert.strictEqual(valid[1].cmd, 'pod');
|
|
154
|
-
});
|
|
155
|
-
it('returns empty array for all-disallowed steps', () => {
|
|
156
|
-
const steps = [{ cmd: 'bash', args: ['-c', 'echo pwned'], cwd: '' }];
|
|
157
|
-
const valid = validateInstallSteps(steps);
|
|
158
|
-
assert.strictEqual(valid.length, 0);
|
|
159
|
-
});
|
|
160
|
-
});
|