@codeyam/codeyam-cli 0.1.29 → 0.1.31
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/analyzer-template/.build-info.json +7 -7
- package/analyzer-template/log.txt +3 -3
- package/analyzer-template/package.json +1 -1
- package/analyzer-template/packages/aws/package.json +1 -1
- package/analyzer-template/packages/database/package.json +1 -1
- package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js +39 -3
- package/codeyam-cli/src/commands/__tests__/init.gitignore.test.js.map +1 -1
- package/codeyam-cli/src/commands/editor.js +133 -0
- package/codeyam-cli/src/commands/editor.js.map +1 -1
- package/codeyam-cli/src/commands/init.js +20 -0
- package/codeyam-cli/src/commands/init.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js +98 -1
- package/codeyam-cli/src/utils/__tests__/editorProxySession.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js +712 -2
- package/codeyam-cli/src/utils/__tests__/editorRoadmap.test.js.map +1 -1
- package/codeyam-cli/src/utils/__tests__/envFile.test.js +125 -0
- package/codeyam-cli/src/utils/__tests__/envFile.test.js.map +1 -0
- package/codeyam-cli/src/utils/__tests__/handoffContext.test.js +500 -0
- package/codeyam-cli/src/utils/__tests__/handoffContext.test.js.map +1 -0
- package/codeyam-cli/src/utils/editorRoadmap.js +290 -17
- package/codeyam-cli/src/utils/editorRoadmap.js.map +1 -1
- package/codeyam-cli/src/utils/envFile.js +90 -0
- package/codeyam-cli/src/utils/envFile.js.map +1 -0
- package/codeyam-cli/src/utils/handoffContext.js +257 -0
- package/codeyam-cli/src/utils/handoffContext.js.map +1 -0
- package/codeyam-cli/src/utils/install-skills.js +36 -6
- package/codeyam-cli/src/utils/install-skills.js.map +1 -1
- package/codeyam-cli/src/utils/techStackConfig.js +38 -0
- package/codeyam-cli/src/utils/techStackConfig.js.map +1 -0
- package/codeyam-cli/src/utils/techStackConfig.test.js +85 -0
- package/codeyam-cli/src/utils/techStackConfig.test.js.map +1 -0
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js +119 -1
- package/codeyam-cli/src/webserver/__tests__/buildPtyEnv.test.js.map +1 -1
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js +115 -0
- package/codeyam-cli/src/webserver/__tests__/editorProxy.test.js.map +1 -1
- package/codeyam-cli/src/webserver/app/lib/database.js.map +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/{MiniClaudeChat-BusrvT2F.js → MiniClaudeChat-Bs2_Oua4.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-database-verify-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-github-verify-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-handoff-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/api.editor-hosting-verify-l0sNRNKZ.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DOXe0Qx7.js +161 -0
- package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-Ce1s4OQ1.js → entity._sha._-pc-vc6wO.js} +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/globals-L-aUIeux.css +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/manifest-30c44d84.js +1 -0
- package/codeyam-cli/src/webserver/build/client/assets/{root-CVjDQwjJ.js → root-CLedrjXQ.js} +7 -7
- package/codeyam-cli/src/webserver/build/server/assets/{analysisRunner-CTJYMVFP.js → analysisRunner-CuR5TvUx.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/{index-CCth4Hgw.js → index-D4MWAsqb.js} +1 -1
- package/codeyam-cli/src/webserver/build/server/assets/init-JObA4lXD.js +14 -0
- package/codeyam-cli/src/webserver/build/server/assets/server-build-i8OXK4oL.js +765 -0
- package/codeyam-cli/src/webserver/build/server/index.js +1 -1
- package/codeyam-cli/src/webserver/build-info.json +5 -5
- package/codeyam-cli/src/webserver/editorProxy.js +77 -4
- package/codeyam-cli/src/webserver/editorProxy.js.map +1 -1
- package/codeyam-cli/src/webserver/terminalServer.js +81 -11
- package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
- package/codeyam-cli/templates/codeyam-editor-codex.md +61 -0
- package/codeyam-cli/templates/codeyam-editor-gemini.md +59 -0
- package/codeyam-cli/templates/nextjs-prisma-sqlite/gitignore +1 -0
- package/codeyam-cli/templates/seed-adapters/supabase.ts +185 -84
- package/package.json +1 -1
- package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-785deXbZ.js +0 -147
- package/codeyam-cli/src/webserver/build/client/assets/globals-Bt7TsgQz.css +0 -1
- package/codeyam-cli/src/webserver/build/client/assets/manifest-3d8cde80.js +0 -1
- package/codeyam-cli/src/webserver/build/server/assets/init-UXl-3vVp.js +0 -10
- package/codeyam-cli/src/webserver/build/server/assets/server-build-DSW2mE30.js +0 -741
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import { readRoadmap, writeRoadmap, getDefaultRoadmap, sanitizeRoadmapData, checkAutoDetections, countJournalEntries, getRecentJournalEntries, } from "../editorRoadmap.js";
|
|
4
|
+
import { readRoadmap, writeRoadmap, getDefaultRoadmap, sanitizeRoadmapData, checkAutoDetections, countJournalEntries, getRecentJournalEntries, generateServiceDeployTasks, } from "../editorRoadmap.js";
|
|
5
5
|
let tmpDir;
|
|
6
6
|
beforeEach(() => {
|
|
7
7
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roadmap-test-'));
|
|
@@ -14,11 +14,16 @@ afterEach(() => {
|
|
|
14
14
|
describe('getDefaultRoadmap', () => {
|
|
15
15
|
it('returns plan and deploy arrays with predefined tasks', () => {
|
|
16
16
|
const defaults = getDefaultRoadmap();
|
|
17
|
-
expect(defaults.plan.length).toBeGreaterThanOrEqual(
|
|
17
|
+
expect(defaults.plan.length).toBeGreaterThanOrEqual(3);
|
|
18
18
|
expect(defaults.deploy.length).toBeGreaterThanOrEqual(5);
|
|
19
19
|
expect(defaults.plan.every((t) => !t.completed)).toBe(true);
|
|
20
20
|
expect(defaults.deploy.every((t) => !t.completed)).toBe(true);
|
|
21
21
|
});
|
|
22
|
+
it('deploy-hosting has autoDetect key in defaults', () => {
|
|
23
|
+
const defaults = getDefaultRoadmap();
|
|
24
|
+
const hosting = defaults.deploy.find((t) => t.id === 'deploy-hosting');
|
|
25
|
+
expect(hosting?.autoDetect).toBe('hosting-configured');
|
|
26
|
+
});
|
|
22
27
|
it('returns fresh copies on each call', () => {
|
|
23
28
|
const a = getDefaultRoadmap();
|
|
24
29
|
const b = getDefaultRoadmap();
|
|
@@ -112,6 +117,21 @@ describe('sanitizeRoadmapData', () => {
|
|
|
112
117
|
expect(result.plan.length).toBeGreaterThan(0);
|
|
113
118
|
expect(result.deploy.length).toBeGreaterThan(0);
|
|
114
119
|
});
|
|
120
|
+
it('patches autoDetect onto existing deploy-hosting tasks from defaults', () => {
|
|
121
|
+
// Simulates an existing roadmap.json that was saved before autoDetect was added
|
|
122
|
+
const result = sanitizeRoadmapData({
|
|
123
|
+
plan: [],
|
|
124
|
+
deploy: [
|
|
125
|
+
{
|
|
126
|
+
id: 'deploy-hosting',
|
|
127
|
+
label: 'Set up hosting provider',
|
|
128
|
+
completed: false,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
const hosting = result.deploy.find((t) => t.id === 'deploy-hosting');
|
|
133
|
+
expect(hosting?.autoDetect).toBe('hosting-configured');
|
|
134
|
+
});
|
|
115
135
|
});
|
|
116
136
|
// ── checkAutoDetections ─────────────────────────────────────────────────
|
|
117
137
|
describe('checkAutoDetections', () => {
|
|
@@ -210,6 +230,87 @@ describe('checkAutoDetections', () => {
|
|
|
210
230
|
expect(result[0].completed).toBe(false);
|
|
211
231
|
expect(result[0].completedAt).toBeUndefined();
|
|
212
232
|
});
|
|
233
|
+
it('marks hosting-configured when config has hosting.provider', () => {
|
|
234
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: { provider: 'railway' } }));
|
|
235
|
+
const todos = [
|
|
236
|
+
{
|
|
237
|
+
id: 'deploy-hosting',
|
|
238
|
+
label: 'Set up hosting provider',
|
|
239
|
+
completed: false,
|
|
240
|
+
autoDetect: 'hosting-configured',
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
244
|
+
expect(result[0].completed).toBe(true);
|
|
245
|
+
expect(result[0].completedAt).toBeDefined();
|
|
246
|
+
});
|
|
247
|
+
it('does not mark hosting-configured when hosting.provider is missing', () => {
|
|
248
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: {} }));
|
|
249
|
+
const todos = [
|
|
250
|
+
{
|
|
251
|
+
id: 'deploy-hosting',
|
|
252
|
+
label: 'Set up hosting provider',
|
|
253
|
+
completed: false,
|
|
254
|
+
autoDetect: 'hosting-configured',
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
258
|
+
expect(result[0].completed).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
it('does not mark hosting-configured when no hosting field exists', () => {
|
|
261
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
|
|
262
|
+
const todos = [
|
|
263
|
+
{
|
|
264
|
+
id: 'deploy-hosting',
|
|
265
|
+
label: 'Set up hosting provider',
|
|
266
|
+
completed: false,
|
|
267
|
+
autoDetect: 'hosting-configured',
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
271
|
+
expect(result[0].completed).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
it('does not mark hosting-configured for vercel without projectId', () => {
|
|
274
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: { provider: 'vercel' } }));
|
|
275
|
+
const todos = [
|
|
276
|
+
{
|
|
277
|
+
id: 'deploy-hosting',
|
|
278
|
+
label: 'Set up hosting provider',
|
|
279
|
+
completed: false,
|
|
280
|
+
autoDetect: 'hosting-configured',
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
284
|
+
expect(result[0].completed).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
it('marks hosting-configured for vercel with projectId', () => {
|
|
287
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
288
|
+
hosting: { provider: 'vercel', vercelProjectId: 'prj_123' },
|
|
289
|
+
}));
|
|
290
|
+
const todos = [
|
|
291
|
+
{
|
|
292
|
+
id: 'deploy-hosting',
|
|
293
|
+
label: 'Set up hosting provider',
|
|
294
|
+
completed: false,
|
|
295
|
+
autoDetect: 'hosting-configured',
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
299
|
+
expect(result[0].completed).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it('marks hosting-configured for non-vercel providers with just provider name', () => {
|
|
302
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({ hosting: { provider: 'railway' } }));
|
|
303
|
+
const todos = [
|
|
304
|
+
{
|
|
305
|
+
id: 'deploy-hosting',
|
|
306
|
+
label: 'Set up hosting provider',
|
|
307
|
+
completed: false,
|
|
308
|
+
autoDetect: 'hosting-configured',
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
312
|
+
expect(result[0].completed).toBe(true);
|
|
313
|
+
});
|
|
213
314
|
it('handles unknown autoDetect keys gracefully', () => {
|
|
214
315
|
const todos = [
|
|
215
316
|
{
|
|
@@ -377,6 +478,13 @@ describe('getRecentJournalEntries', () => {
|
|
|
377
478
|
title: 'Add checkout page',
|
|
378
479
|
time: '2026-03-30T10:00:00Z',
|
|
379
480
|
type: 'feature',
|
|
481
|
+
description: 'Built the checkout flow',
|
|
482
|
+
userPrompt: undefined,
|
|
483
|
+
scenarioScreenshots: undefined,
|
|
484
|
+
modifiedFiles: undefined,
|
|
485
|
+
entityChangeStatus: undefined,
|
|
486
|
+
commitSha: 'abc123',
|
|
487
|
+
commitMessage: 'Add checkout page',
|
|
380
488
|
});
|
|
381
489
|
});
|
|
382
490
|
it('defaults limit to 3', () => {
|
|
@@ -395,4 +503,606 @@ describe('getRecentJournalEntries', () => {
|
|
|
395
503
|
expect(result).toHaveLength(3);
|
|
396
504
|
});
|
|
397
505
|
});
|
|
506
|
+
// ── generateServiceDeployTasks ─────────────────────────────────────────
|
|
507
|
+
describe('generateServiceDeployTasks', () => {
|
|
508
|
+
it('returns empty array when tech stack has no services with envKeys', () => {
|
|
509
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
510
|
+
techStack: {
|
|
511
|
+
languages: [{ name: 'TypeScript', url: '', description: '' }],
|
|
512
|
+
frameworks: [{ name: 'Next.js', url: '', description: '' }],
|
|
513
|
+
libraries: [{ name: 'Tailwind', url: '', description: '' }],
|
|
514
|
+
},
|
|
515
|
+
}));
|
|
516
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
517
|
+
expect(tasks).toEqual([]);
|
|
518
|
+
});
|
|
519
|
+
it('returns empty array when no tech stack exists', () => {
|
|
520
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
|
|
521
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
522
|
+
expect(tasks).toEqual([]);
|
|
523
|
+
});
|
|
524
|
+
it('returns empty array when config.json does not exist', () => {
|
|
525
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
526
|
+
expect(tasks).toEqual([]);
|
|
527
|
+
});
|
|
528
|
+
it('generates tasks for databases with envKeys', () => {
|
|
529
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
530
|
+
techStack: {
|
|
531
|
+
databases: [
|
|
532
|
+
{
|
|
533
|
+
name: 'Neon',
|
|
534
|
+
url: 'https://neon.tech',
|
|
535
|
+
description: 'Serverless PostgreSQL',
|
|
536
|
+
envKeys: ['DATABASE_URL', 'DIRECT_URL'],
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
}));
|
|
541
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
542
|
+
expect(tasks).toHaveLength(1);
|
|
543
|
+
expect(tasks[0].id).toBe('deploy-svc-neon');
|
|
544
|
+
expect(tasks[0].label).toBe('Set up Neon');
|
|
545
|
+
expect(tasks[0].autoDetect).toBe('svc-envkeys-neon');
|
|
546
|
+
expect(tasks[0].serviceRef).toEqual({
|
|
547
|
+
name: 'Neon',
|
|
548
|
+
category: 'databases',
|
|
549
|
+
});
|
|
550
|
+
expect(tasks[0].completed).toBe(false);
|
|
551
|
+
});
|
|
552
|
+
it('generates tasks for services with envKeys', () => {
|
|
553
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
554
|
+
techStack: {
|
|
555
|
+
services: [
|
|
556
|
+
{
|
|
557
|
+
name: 'Stripe',
|
|
558
|
+
url: 'https://stripe.com',
|
|
559
|
+
description: 'Payments',
|
|
560
|
+
envKeys: ['STRIPE_SECRET_KEY', 'STRIPE_PUBLISHABLE_KEY'],
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
},
|
|
564
|
+
}));
|
|
565
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
566
|
+
expect(tasks).toHaveLength(1);
|
|
567
|
+
expect(tasks[0].id).toBe('deploy-svc-stripe');
|
|
568
|
+
expect(tasks[0].label).toBe('Set up Stripe');
|
|
569
|
+
expect(tasks[0].serviceRef).toEqual({
|
|
570
|
+
name: 'Stripe',
|
|
571
|
+
category: 'services',
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
it('generates tasks for infrastructure with envKeys', () => {
|
|
575
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
576
|
+
techStack: {
|
|
577
|
+
infrastructure: [
|
|
578
|
+
{
|
|
579
|
+
name: 'AWS S3',
|
|
580
|
+
url: 'https://aws.amazon.com/s3',
|
|
581
|
+
description: 'Object storage',
|
|
582
|
+
envKeys: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
}));
|
|
587
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
588
|
+
expect(tasks).toHaveLength(1);
|
|
589
|
+
expect(tasks[0].id).toBe('deploy-svc-aws-s3');
|
|
590
|
+
expect(tasks[0].label).toBe('Set up AWS S3');
|
|
591
|
+
});
|
|
592
|
+
it('skips services with empty envKeys arrays', () => {
|
|
593
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
594
|
+
techStack: {
|
|
595
|
+
services: [
|
|
596
|
+
{
|
|
597
|
+
name: 'Stripe',
|
|
598
|
+
url: '',
|
|
599
|
+
description: '',
|
|
600
|
+
envKeys: ['STRIPE_KEY'],
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'SomeLib',
|
|
604
|
+
url: '',
|
|
605
|
+
description: '',
|
|
606
|
+
envKeys: [],
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
}));
|
|
611
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
612
|
+
expect(tasks).toHaveLength(1);
|
|
613
|
+
expect(tasks[0].id).toBe('deploy-svc-stripe');
|
|
614
|
+
});
|
|
615
|
+
it('generates multiple tasks across categories', () => {
|
|
616
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
617
|
+
techStack: {
|
|
618
|
+
databases: [
|
|
619
|
+
{
|
|
620
|
+
name: 'PostgreSQL',
|
|
621
|
+
url: '',
|
|
622
|
+
description: '',
|
|
623
|
+
envKeys: ['DATABASE_URL'],
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
services: [
|
|
627
|
+
{
|
|
628
|
+
name: 'Resend',
|
|
629
|
+
url: '',
|
|
630
|
+
description: '',
|
|
631
|
+
envKeys: ['RESEND_API_KEY'],
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
},
|
|
635
|
+
}));
|
|
636
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
637
|
+
expect(tasks).toHaveLength(2);
|
|
638
|
+
const ids = tasks.map((t) => t.id);
|
|
639
|
+
expect(ids).toContain('deploy-svc-postgresql');
|
|
640
|
+
expect(ids).toContain('deploy-svc-resend');
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
// ── readRoadmap with dynamic service tasks ────────────────────────────
|
|
644
|
+
describe('readRoadmap with dynamic service tasks', () => {
|
|
645
|
+
it('includes dynamic service tasks in deploy list', () => {
|
|
646
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
647
|
+
techStack: {
|
|
648
|
+
services: [
|
|
649
|
+
{
|
|
650
|
+
name: 'Stripe',
|
|
651
|
+
url: '',
|
|
652
|
+
description: '',
|
|
653
|
+
envKeys: ['STRIPE_KEY'],
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
},
|
|
657
|
+
}));
|
|
658
|
+
const data = readRoadmap(tmpDir);
|
|
659
|
+
const stripeTask = data.deploy.find((t) => t.id === 'deploy-svc-stripe');
|
|
660
|
+
expect(stripeTask).toBeDefined();
|
|
661
|
+
expect(stripeTask.label).toBe('Set up Stripe');
|
|
662
|
+
});
|
|
663
|
+
it('inserts service tasks after deploy-hosting and before deploy-env-vars', () => {
|
|
664
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
665
|
+
techStack: {
|
|
666
|
+
services: [
|
|
667
|
+
{
|
|
668
|
+
name: 'Stripe',
|
|
669
|
+
url: '',
|
|
670
|
+
description: '',
|
|
671
|
+
envKeys: ['STRIPE_KEY'],
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
}));
|
|
676
|
+
const data = readRoadmap(tmpDir);
|
|
677
|
+
const ids = data.deploy.map((t) => t.id);
|
|
678
|
+
const hostingIdx = ids.indexOf('deploy-hosting');
|
|
679
|
+
const envVarsIdx = ids.indexOf('deploy-env-vars');
|
|
680
|
+
const stripeIdx = ids.indexOf('deploy-svc-stripe');
|
|
681
|
+
expect(hostingIdx).toBeGreaterThanOrEqual(0);
|
|
682
|
+
expect(envVarsIdx).toBeGreaterThanOrEqual(0);
|
|
683
|
+
expect(stripeIdx).toBeGreaterThan(hostingIdx);
|
|
684
|
+
expect(stripeIdx).toBeLessThan(envVarsIdx);
|
|
685
|
+
});
|
|
686
|
+
it('preserves completion status of persisted service tasks', () => {
|
|
687
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
688
|
+
techStack: {
|
|
689
|
+
services: [
|
|
690
|
+
{
|
|
691
|
+
name: 'Stripe',
|
|
692
|
+
url: '',
|
|
693
|
+
description: '',
|
|
694
|
+
envKeys: ['STRIPE_KEY'],
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
}));
|
|
699
|
+
// Write roadmap with stripe task already completed
|
|
700
|
+
const defaults = getDefaultRoadmap();
|
|
701
|
+
defaults.deploy.push({
|
|
702
|
+
id: 'deploy-svc-stripe',
|
|
703
|
+
label: 'Set up Stripe',
|
|
704
|
+
completed: true,
|
|
705
|
+
completedAt: '2026-03-30T00:00:00Z',
|
|
706
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
707
|
+
});
|
|
708
|
+
writeRoadmap(tmpDir, defaults);
|
|
709
|
+
const data = readRoadmap(tmpDir);
|
|
710
|
+
const stripeTask = data.deploy.find((t) => t.id === 'deploy-svc-stripe');
|
|
711
|
+
expect(stripeTask).toBeDefined();
|
|
712
|
+
expect(stripeTask.completed).toBe(true);
|
|
713
|
+
expect(stripeTask.completedAt).toBe('2026-03-30T00:00:00Z');
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
// ── Dynamic service auto-detection ────────────────────────────────────
|
|
717
|
+
describe('dynamic service auto-detection', () => {
|
|
718
|
+
it('marks service task complete when all envKeys are in environmentVariables', () => {
|
|
719
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
720
|
+
techStack: {
|
|
721
|
+
services: [
|
|
722
|
+
{
|
|
723
|
+
name: 'Stripe',
|
|
724
|
+
url: '',
|
|
725
|
+
description: '',
|
|
726
|
+
envKeys: ['STRIPE_SECRET_KEY', 'STRIPE_PUBLISHABLE_KEY'],
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
},
|
|
730
|
+
environmentVariables: [
|
|
731
|
+
{ key: 'STRIPE_SECRET_KEY', value: 'sk_test_123' },
|
|
732
|
+
{ key: 'STRIPE_PUBLISHABLE_KEY', value: 'pk_test_456' },
|
|
733
|
+
],
|
|
734
|
+
}));
|
|
735
|
+
const todos = [
|
|
736
|
+
{
|
|
737
|
+
id: 'deploy-svc-stripe',
|
|
738
|
+
label: 'Set up Stripe',
|
|
739
|
+
completed: false,
|
|
740
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
741
|
+
},
|
|
742
|
+
];
|
|
743
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
744
|
+
expect(result[0].completed).toBe(true);
|
|
745
|
+
expect(result[0].completedAt).toBeDefined();
|
|
746
|
+
});
|
|
747
|
+
it('does not mark service task complete when some envKeys are missing', () => {
|
|
748
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
749
|
+
techStack: {
|
|
750
|
+
services: [
|
|
751
|
+
{
|
|
752
|
+
name: 'Stripe',
|
|
753
|
+
url: '',
|
|
754
|
+
description: '',
|
|
755
|
+
envKeys: ['STRIPE_SECRET_KEY', 'STRIPE_PUBLISHABLE_KEY'],
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
},
|
|
759
|
+
environmentVariables: [
|
|
760
|
+
{ key: 'STRIPE_SECRET_KEY', value: 'sk_test_123' },
|
|
761
|
+
],
|
|
762
|
+
}));
|
|
763
|
+
const todos = [
|
|
764
|
+
{
|
|
765
|
+
id: 'deploy-svc-stripe',
|
|
766
|
+
label: 'Set up Stripe',
|
|
767
|
+
completed: false,
|
|
768
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
769
|
+
},
|
|
770
|
+
];
|
|
771
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
772
|
+
expect(result[0].completed).toBe(false);
|
|
773
|
+
});
|
|
774
|
+
it('does not mark service task complete when envKey has empty value', () => {
|
|
775
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
776
|
+
techStack: {
|
|
777
|
+
services: [
|
|
778
|
+
{
|
|
779
|
+
name: 'Stripe',
|
|
780
|
+
url: '',
|
|
781
|
+
description: '',
|
|
782
|
+
envKeys: ['STRIPE_KEY'],
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
},
|
|
786
|
+
environmentVariables: [{ key: 'STRIPE_KEY', value: '' }],
|
|
787
|
+
}));
|
|
788
|
+
const todos = [
|
|
789
|
+
{
|
|
790
|
+
id: 'deploy-svc-stripe',
|
|
791
|
+
label: 'Set up Stripe',
|
|
792
|
+
completed: false,
|
|
793
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
794
|
+
},
|
|
795
|
+
];
|
|
796
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
797
|
+
expect(result[0].completed).toBe(false);
|
|
798
|
+
});
|
|
799
|
+
it('handles "name" field in environmentVariables (legacy format)', () => {
|
|
800
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
801
|
+
techStack: {
|
|
802
|
+
services: [
|
|
803
|
+
{
|
|
804
|
+
name: 'Stripe',
|
|
805
|
+
url: '',
|
|
806
|
+
description: '',
|
|
807
|
+
envKeys: ['STRIPE_KEY'],
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
},
|
|
811
|
+
environmentVariables: [{ name: 'STRIPE_KEY', value: 'sk_test_123' }],
|
|
812
|
+
}));
|
|
813
|
+
const todos = [
|
|
814
|
+
{
|
|
815
|
+
id: 'deploy-svc-stripe',
|
|
816
|
+
label: 'Set up Stripe',
|
|
817
|
+
completed: false,
|
|
818
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
819
|
+
},
|
|
820
|
+
];
|
|
821
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
822
|
+
expect(result[0].completed).toBe(true);
|
|
823
|
+
});
|
|
824
|
+
it('un-completes service task when envKeys are removed from config', () => {
|
|
825
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
826
|
+
techStack: {
|
|
827
|
+
services: [
|
|
828
|
+
{
|
|
829
|
+
name: 'Stripe',
|
|
830
|
+
url: '',
|
|
831
|
+
description: '',
|
|
832
|
+
envKeys: ['STRIPE_KEY'],
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
},
|
|
836
|
+
environmentVariables: [],
|
|
837
|
+
}));
|
|
838
|
+
const todos = [
|
|
839
|
+
{
|
|
840
|
+
id: 'deploy-svc-stripe',
|
|
841
|
+
label: 'Set up Stripe',
|
|
842
|
+
completed: true,
|
|
843
|
+
completedAt: '2026-01-01',
|
|
844
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
845
|
+
},
|
|
846
|
+
];
|
|
847
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
848
|
+
expect(result[0].completed).toBe(false);
|
|
849
|
+
expect(result[0].completedAt).toBeUndefined();
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
// ── env-vars-configured auto-detection ────────────────────────────────
|
|
853
|
+
describe('env-vars-configured auto-detection', () => {
|
|
854
|
+
it('marks deploy-env-vars complete when all service envKeys are configured', () => {
|
|
855
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
856
|
+
techStack: {
|
|
857
|
+
databases: [
|
|
858
|
+
{ name: 'Supabase', url: '', description: '', envKeys: ['DB_URL'] },
|
|
859
|
+
],
|
|
860
|
+
services: [
|
|
861
|
+
{
|
|
862
|
+
name: 'Stripe',
|
|
863
|
+
url: '',
|
|
864
|
+
description: '',
|
|
865
|
+
envKeys: ['STRIPE_KEY'],
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
},
|
|
869
|
+
environmentVariables: [
|
|
870
|
+
{ key: 'DB_URL', value: 'postgresql://...' },
|
|
871
|
+
{ key: 'STRIPE_KEY', value: 'sk_test_123' },
|
|
872
|
+
],
|
|
873
|
+
}));
|
|
874
|
+
const todos = [
|
|
875
|
+
{
|
|
876
|
+
id: 'deploy-env-vars',
|
|
877
|
+
label: 'Configure environment variables',
|
|
878
|
+
completed: false,
|
|
879
|
+
autoDetect: 'env-vars-configured',
|
|
880
|
+
},
|
|
881
|
+
];
|
|
882
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
883
|
+
expect(result[0].completed).toBe(true);
|
|
884
|
+
});
|
|
885
|
+
it('does not mark deploy-env-vars complete when some envKeys are missing', () => {
|
|
886
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
887
|
+
techStack: {
|
|
888
|
+
services: [
|
|
889
|
+
{
|
|
890
|
+
name: 'Stripe',
|
|
891
|
+
url: '',
|
|
892
|
+
description: '',
|
|
893
|
+
envKeys: ['STRIPE_SECRET', 'STRIPE_PUBLIC'],
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
},
|
|
897
|
+
environmentVariables: [{ key: 'STRIPE_SECRET', value: 'sk_test_123' }],
|
|
898
|
+
}));
|
|
899
|
+
const todos = [
|
|
900
|
+
{
|
|
901
|
+
id: 'deploy-env-vars',
|
|
902
|
+
label: 'Configure environment variables',
|
|
903
|
+
completed: false,
|
|
904
|
+
autoDetect: 'env-vars-configured',
|
|
905
|
+
},
|
|
906
|
+
];
|
|
907
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
908
|
+
expect(result[0].completed).toBe(false);
|
|
909
|
+
});
|
|
910
|
+
it('marks deploy-env-vars complete when no services have envKeys', () => {
|
|
911
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
912
|
+
techStack: {
|
|
913
|
+
languages: [{ name: 'TypeScript', url: '', description: '' }],
|
|
914
|
+
},
|
|
915
|
+
}));
|
|
916
|
+
const todos = [
|
|
917
|
+
{
|
|
918
|
+
id: 'deploy-env-vars',
|
|
919
|
+
label: 'Configure environment variables',
|
|
920
|
+
completed: false,
|
|
921
|
+
autoDetect: 'env-vars-configured',
|
|
922
|
+
},
|
|
923
|
+
];
|
|
924
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
925
|
+
expect(result[0].completed).toBe(true);
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
// ── deploy-database defaults and auto-detection ─────────────────────
|
|
929
|
+
describe('deploy-database', () => {
|
|
930
|
+
it('deploy-database has autoDetect key in defaults', () => {
|
|
931
|
+
const defaults = getDefaultRoadmap();
|
|
932
|
+
const db = defaults.deploy.find((t) => t.id === 'deploy-database');
|
|
933
|
+
expect(db).toBeDefined();
|
|
934
|
+
expect(db.autoDetect).toBe('database-configured');
|
|
935
|
+
expect(db.label).toBe('Set up hosted database');
|
|
936
|
+
});
|
|
937
|
+
it('deploy-database appears after deploy-hosting in defaults', () => {
|
|
938
|
+
const defaults = getDefaultRoadmap();
|
|
939
|
+
const ids = defaults.deploy.map((t) => t.id);
|
|
940
|
+
const hostingIdx = ids.indexOf('deploy-hosting');
|
|
941
|
+
const dbIdx = ids.indexOf('deploy-database');
|
|
942
|
+
expect(dbIdx).toBe(hostingIdx + 1);
|
|
943
|
+
});
|
|
944
|
+
it('marks database-configured when Supabase provider with projectRef and all env vars', () => {
|
|
945
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
946
|
+
database: {
|
|
947
|
+
provider: 'supabase',
|
|
948
|
+
supabaseProjectRef: 'xyzref',
|
|
949
|
+
},
|
|
950
|
+
environmentVariables: [
|
|
951
|
+
{
|
|
952
|
+
key: 'NEXT_PUBLIC_SUPABASE_URL',
|
|
953
|
+
value: 'https://xyzref.supabase.co',
|
|
954
|
+
},
|
|
955
|
+
{ key: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', value: 'eyJ...' },
|
|
956
|
+
{ key: 'DATABASE_URL', value: 'postgresql://...' },
|
|
957
|
+
],
|
|
958
|
+
}));
|
|
959
|
+
const todos = [
|
|
960
|
+
{
|
|
961
|
+
id: 'deploy-database',
|
|
962
|
+
label: 'Set up hosted database',
|
|
963
|
+
completed: false,
|
|
964
|
+
autoDetect: 'database-configured',
|
|
965
|
+
},
|
|
966
|
+
];
|
|
967
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
968
|
+
expect(result[0].completed).toBe(true);
|
|
969
|
+
expect(result[0].completedAt).toBeDefined();
|
|
970
|
+
});
|
|
971
|
+
it('does not mark database-configured when provider is missing', () => {
|
|
972
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({}));
|
|
973
|
+
const todos = [
|
|
974
|
+
{
|
|
975
|
+
id: 'deploy-database',
|
|
976
|
+
label: 'Set up hosted database',
|
|
977
|
+
completed: false,
|
|
978
|
+
autoDetect: 'database-configured',
|
|
979
|
+
},
|
|
980
|
+
];
|
|
981
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
982
|
+
expect(result[0].completed).toBe(false);
|
|
983
|
+
});
|
|
984
|
+
it('does not mark database-configured for Supabase without projectRef', () => {
|
|
985
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
986
|
+
database: { provider: 'supabase' },
|
|
987
|
+
environmentVariables: [
|
|
988
|
+
{ key: 'NEXT_PUBLIC_SUPABASE_URL', value: 'https://x.supabase.co' },
|
|
989
|
+
{ key: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', value: 'key' },
|
|
990
|
+
{ key: 'DATABASE_URL', value: 'postgresql://...' },
|
|
991
|
+
],
|
|
992
|
+
}));
|
|
993
|
+
const todos = [
|
|
994
|
+
{
|
|
995
|
+
id: 'deploy-database',
|
|
996
|
+
label: 'Set up hosted database',
|
|
997
|
+
completed: false,
|
|
998
|
+
autoDetect: 'database-configured',
|
|
999
|
+
},
|
|
1000
|
+
];
|
|
1001
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
1002
|
+
expect(result[0].completed).toBe(false);
|
|
1003
|
+
});
|
|
1004
|
+
it('does not mark database-configured for Supabase with missing env vars', () => {
|
|
1005
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
1006
|
+
database: {
|
|
1007
|
+
provider: 'supabase',
|
|
1008
|
+
supabaseProjectRef: 'xyzref',
|
|
1009
|
+
},
|
|
1010
|
+
environmentVariables: [
|
|
1011
|
+
{
|
|
1012
|
+
key: 'NEXT_PUBLIC_SUPABASE_URL',
|
|
1013
|
+
value: 'https://xyzref.supabase.co',
|
|
1014
|
+
},
|
|
1015
|
+
// Missing ANON_KEY and DATABASE_URL
|
|
1016
|
+
],
|
|
1017
|
+
}));
|
|
1018
|
+
const todos = [
|
|
1019
|
+
{
|
|
1020
|
+
id: 'deploy-database',
|
|
1021
|
+
label: 'Set up hosted database',
|
|
1022
|
+
completed: false,
|
|
1023
|
+
autoDetect: 'database-configured',
|
|
1024
|
+
},
|
|
1025
|
+
];
|
|
1026
|
+
const result = checkAutoDetections(tmpDir, todos);
|
|
1027
|
+
expect(result[0].completed).toBe(false);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
// ── service task insertion with deploy-database ─────────────────────
|
|
1031
|
+
describe('service tasks insert after deploy-database', () => {
|
|
1032
|
+
it('inserts service tasks after deploy-database and before deploy-env-vars', () => {
|
|
1033
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
1034
|
+
techStack: {
|
|
1035
|
+
services: [
|
|
1036
|
+
{
|
|
1037
|
+
name: 'Stripe',
|
|
1038
|
+
url: '',
|
|
1039
|
+
description: '',
|
|
1040
|
+
envKeys: ['STRIPE_KEY'],
|
|
1041
|
+
},
|
|
1042
|
+
],
|
|
1043
|
+
},
|
|
1044
|
+
}));
|
|
1045
|
+
const data = readRoadmap(tmpDir);
|
|
1046
|
+
const ids = data.deploy.map((t) => t.id);
|
|
1047
|
+
const dbIdx = ids.indexOf('deploy-database');
|
|
1048
|
+
const envVarsIdx = ids.indexOf('deploy-env-vars');
|
|
1049
|
+
const stripeIdx = ids.indexOf('deploy-svc-stripe');
|
|
1050
|
+
expect(dbIdx).toBeGreaterThanOrEqual(0);
|
|
1051
|
+
expect(stripeIdx).toBeGreaterThan(dbIdx);
|
|
1052
|
+
expect(stripeIdx).toBeLessThan(envVarsIdx);
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
// ── Supabase exclusion from dynamic service tasks ────────────────────
|
|
1056
|
+
describe('generateServiceDeployTasks Supabase exclusion', () => {
|
|
1057
|
+
it('excludes Supabase from generated service tasks', () => {
|
|
1058
|
+
fs.writeFileSync(path.join(tmpDir, '.codeyam', 'config.json'), JSON.stringify({
|
|
1059
|
+
techStack: {
|
|
1060
|
+
databases: [
|
|
1061
|
+
{
|
|
1062
|
+
name: 'Supabase',
|
|
1063
|
+
url: 'https://supabase.com',
|
|
1064
|
+
description: 'Hosted PostgreSQL',
|
|
1065
|
+
envKeys: [
|
|
1066
|
+
'NEXT_PUBLIC_SUPABASE_URL',
|
|
1067
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
1068
|
+
'DATABASE_URL',
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
],
|
|
1072
|
+
services: [
|
|
1073
|
+
{
|
|
1074
|
+
name: 'Stripe',
|
|
1075
|
+
url: '',
|
|
1076
|
+
description: '',
|
|
1077
|
+
envKeys: ['STRIPE_KEY'],
|
|
1078
|
+
},
|
|
1079
|
+
],
|
|
1080
|
+
},
|
|
1081
|
+
}));
|
|
1082
|
+
const tasks = generateServiceDeployTasks(tmpDir);
|
|
1083
|
+
// Supabase should be excluded (handled by deploy-database), Stripe should remain
|
|
1084
|
+
expect(tasks).toHaveLength(1);
|
|
1085
|
+
expect(tasks[0].id).toBe('deploy-svc-stripe');
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
// ── sanitizeRoadmapData preserves serviceRef ──────────────────────────
|
|
1089
|
+
describe('sanitizeRoadmapData with serviceRef', () => {
|
|
1090
|
+
it('preserves serviceRef field on tasks', () => {
|
|
1091
|
+
const result = sanitizeRoadmapData({
|
|
1092
|
+
plan: [],
|
|
1093
|
+
deploy: [
|
|
1094
|
+
{
|
|
1095
|
+
id: 'deploy-svc-stripe',
|
|
1096
|
+
label: 'Set up Stripe',
|
|
1097
|
+
completed: false,
|
|
1098
|
+
autoDetect: 'svc-envkeys-stripe',
|
|
1099
|
+
serviceRef: { name: 'Stripe', category: 'services' },
|
|
1100
|
+
},
|
|
1101
|
+
],
|
|
1102
|
+
});
|
|
1103
|
+
const task = result.deploy.find((t) => t.id === 'deploy-svc-stripe');
|
|
1104
|
+
expect(task).toBeDefined();
|
|
1105
|
+
expect(task.serviceRef).toEqual({ name: 'Stripe', category: 'services' });
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
398
1108
|
//# sourceMappingURL=editorRoadmap.test.js.map
|