@positronic/cli 0.0.2
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/src/cli.js +739 -0
- package/dist/src/commands/backend.js +199 -0
- package/dist/src/commands/brain.js +446 -0
- package/dist/src/commands/brain.test.js +2936 -0
- package/dist/src/commands/helpers.js +1315 -0
- package/dist/src/commands/helpers.test.js +832 -0
- package/dist/src/commands/project-config-manager.js +197 -0
- package/dist/src/commands/project.js +130 -0
- package/dist/src/commands/project.test.js +1201 -0
- package/dist/src/commands/resources.js +272 -0
- package/dist/src/commands/resources.test.js +2511 -0
- package/dist/src/commands/schedule.js +73 -0
- package/dist/src/commands/schedule.test.js +1235 -0
- package/dist/src/commands/secret.js +87 -0
- package/dist/src/commands/secret.test.d.js +1 -0
- package/dist/src/commands/secret.test.js +761 -0
- package/dist/src/commands/server.js +816 -0
- package/dist/src/commands/server.test.js +1237 -0
- package/dist/src/commands/test-utils.js +737 -0
- package/dist/src/components/brain-history.js +169 -0
- package/dist/src/components/brain-list.js +108 -0
- package/dist/src/components/brain-rerun.js +313 -0
- package/dist/src/components/brain-show.js +65 -0
- package/dist/src/components/error.js +19 -0
- package/dist/src/components/project-add.js +95 -0
- package/dist/src/components/project-create.js +276 -0
- package/dist/src/components/project-list.js +88 -0
- package/dist/src/components/project-remove.js +91 -0
- package/dist/src/components/project-select.js +224 -0
- package/dist/src/components/project-show.js +41 -0
- package/dist/src/components/resource-clear.js +152 -0
- package/dist/src/components/resource-delete.js +189 -0
- package/dist/src/components/resource-list.js +174 -0
- package/dist/src/components/resource-sync.js +386 -0
- package/dist/src/components/resource-types.js +243 -0
- package/dist/src/components/resource-upload.js +366 -0
- package/dist/src/components/schedule-create.js +259 -0
- package/dist/src/components/schedule-delete.js +161 -0
- package/dist/src/components/schedule-list.js +176 -0
- package/dist/src/components/schedule-runs.js +103 -0
- package/dist/src/components/secret-bulk.js +262 -0
- package/dist/src/components/secret-create.js +199 -0
- package/dist/src/components/secret-delete.js +190 -0
- package/dist/src/components/secret-list.js +190 -0
- package/dist/src/components/secret-sync.js +303 -0
- package/dist/src/components/watch.js +184 -0
- package/dist/src/hooks/useApi.js +512 -0
- package/dist/src/positronic.js +33 -0
- package/dist/src/test/mock-api-client.js +371 -0
- package/dist/src/test/test-dev-server.js +1376 -0
- package/dist/types/cli.d.ts +9 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/commands/backend.d.ts +6 -0
- package/dist/types/commands/backend.d.ts.map +1 -0
- package/dist/types/commands/brain.d.ts +35 -0
- package/dist/types/commands/brain.d.ts.map +1 -0
- package/dist/types/commands/helpers.d.ts +55 -0
- package/dist/types/commands/helpers.d.ts.map +1 -0
- package/dist/types/commands/project-config-manager.d.ts +37 -0
- package/dist/types/commands/project-config-manager.d.ts.map +1 -0
- package/dist/types/commands/project.d.ts +55 -0
- package/dist/types/commands/project.d.ts.map +1 -0
- package/dist/types/commands/resources.d.ts +13 -0
- package/dist/types/commands/resources.d.ts.map +1 -0
- package/dist/types/commands/schedule.d.ts +27 -0
- package/dist/types/commands/schedule.d.ts.map +1 -0
- package/dist/types/commands/secret.d.ts +23 -0
- package/dist/types/commands/secret.d.ts.map +1 -0
- package/dist/types/commands/server.d.ts +12 -0
- package/dist/types/commands/server.d.ts.map +1 -0
- package/dist/types/commands/test-utils.d.ts +45 -0
- package/dist/types/commands/test-utils.d.ts.map +1 -0
- package/dist/types/components/brain-history.d.ts +7 -0
- package/dist/types/components/brain-history.d.ts.map +1 -0
- package/dist/types/components/brain-list.d.ts +2 -0
- package/dist/types/components/brain-list.d.ts.map +1 -0
- package/dist/types/components/brain-rerun.d.ts +9 -0
- package/dist/types/components/brain-rerun.d.ts.map +1 -0
- package/dist/types/components/brain-show.d.ts +6 -0
- package/dist/types/components/brain-show.d.ts.map +1 -0
- package/dist/types/components/error.d.ts +10 -0
- package/dist/types/components/error.d.ts.map +1 -0
- package/dist/types/components/project-add.d.ts +9 -0
- package/dist/types/components/project-add.d.ts.map +1 -0
- package/dist/types/components/project-create.d.ts +6 -0
- package/dist/types/components/project-create.d.ts.map +1 -0
- package/dist/types/components/project-list.d.ts +7 -0
- package/dist/types/components/project-list.d.ts.map +1 -0
- package/dist/types/components/project-remove.d.ts +8 -0
- package/dist/types/components/project-remove.d.ts.map +1 -0
- package/dist/types/components/project-select.d.ts +8 -0
- package/dist/types/components/project-select.d.ts.map +1 -0
- package/dist/types/components/project-show.d.ts +7 -0
- package/dist/types/components/project-show.d.ts.map +1 -0
- package/dist/types/components/resource-clear.d.ts +2 -0
- package/dist/types/components/resource-clear.d.ts.map +1 -0
- package/dist/types/components/resource-delete.d.ts +9 -0
- package/dist/types/components/resource-delete.d.ts.map +1 -0
- package/dist/types/components/resource-list.d.ts +2 -0
- package/dist/types/components/resource-list.d.ts.map +1 -0
- package/dist/types/components/resource-sync.d.ts +8 -0
- package/dist/types/components/resource-sync.d.ts.map +1 -0
- package/dist/types/components/resource-types.d.ts +7 -0
- package/dist/types/components/resource-types.d.ts.map +1 -0
- package/dist/types/components/resource-upload.d.ts +8 -0
- package/dist/types/components/resource-upload.d.ts.map +1 -0
- package/dist/types/components/schedule-create.d.ts +7 -0
- package/dist/types/components/schedule-create.d.ts.map +1 -0
- package/dist/types/components/schedule-delete.d.ts +7 -0
- package/dist/types/components/schedule-delete.d.ts.map +1 -0
- package/dist/types/components/schedule-list.d.ts +6 -0
- package/dist/types/components/schedule-list.d.ts.map +1 -0
- package/dist/types/components/schedule-runs.d.ts +8 -0
- package/dist/types/components/schedule-runs.d.ts.map +1 -0
- package/dist/types/components/secret-bulk.d.ts +8 -0
- package/dist/types/components/secret-bulk.d.ts.map +1 -0
- package/dist/types/components/secret-create.d.ts +9 -0
- package/dist/types/components/secret-create.d.ts.map +1 -0
- package/dist/types/components/secret-delete.d.ts +8 -0
- package/dist/types/components/secret-delete.d.ts.map +1 -0
- package/dist/types/components/secret-list.d.ts +7 -0
- package/dist/types/components/secret-list.d.ts.map +1 -0
- package/dist/types/components/secret-sync.d.ts +9 -0
- package/dist/types/components/secret-sync.d.ts.map +1 -0
- package/dist/types/components/watch.d.ts +7 -0
- package/dist/types/components/watch.d.ts.map +1 -0
- package/dist/types/hooks/useApi.d.ts +29 -0
- package/dist/types/hooks/useApi.d.ts.map +1 -0
- package/dist/types/positronic.d.ts +3 -0
- package/dist/types/positronic.d.ts.map +1 -0
- package/dist/types/test/mock-api-client.d.ts +25 -0
- package/dist/types/test/mock-api-client.d.ts.map +1 -0
- package/dist/types/test/test-dev-server.d.ts +129 -0
- package/dist/types/test/test-dev-server.d.ts.map +1 -0
- package/package.json +37 -0
- package/src/cli.ts +981 -0
- package/src/commands/backend.ts +63 -0
- package/src/commands/brain.test.ts +1004 -0
- package/src/commands/brain.ts +215 -0
- package/src/commands/helpers.test.ts +487 -0
- package/src/commands/helpers.ts +870 -0
- package/src/commands/project-config-manager.ts +152 -0
- package/src/commands/project.test.ts +502 -0
- package/src/commands/project.ts +109 -0
- package/src/commands/resources.test.ts +1052 -0
- package/src/commands/resources.ts +97 -0
- package/src/commands/schedule.test.ts +481 -0
- package/src/commands/schedule.ts +65 -0
- package/src/commands/secret.test.ts +210 -0
- package/src/commands/secret.ts +50 -0
- package/src/commands/server.test.ts +493 -0
- package/src/commands/server.ts +353 -0
- package/src/commands/test-utils.ts +324 -0
- package/src/components/brain-history.tsx +198 -0
- package/src/components/brain-list.tsx +105 -0
- package/src/components/brain-rerun.tsx +111 -0
- package/src/components/brain-show.tsx +92 -0
- package/src/components/error.tsx +24 -0
- package/src/components/project-add.tsx +59 -0
- package/src/components/project-create.tsx +83 -0
- package/src/components/project-list.tsx +83 -0
- package/src/components/project-remove.tsx +55 -0
- package/src/components/project-select.tsx +200 -0
- package/src/components/project-show.tsx +58 -0
- package/src/components/resource-clear.tsx +127 -0
- package/src/components/resource-delete.tsx +160 -0
- package/src/components/resource-list.tsx +177 -0
- package/src/components/resource-sync.tsx +170 -0
- package/src/components/resource-types.tsx +55 -0
- package/src/components/resource-upload.tsx +182 -0
- package/src/components/schedule-create.tsx +90 -0
- package/src/components/schedule-delete.tsx +116 -0
- package/src/components/schedule-list.tsx +186 -0
- package/src/components/schedule-runs.tsx +151 -0
- package/src/components/secret-bulk.tsx +79 -0
- package/src/components/secret-create.tsx +49 -0
- package/src/components/secret-delete.tsx +41 -0
- package/src/components/secret-list.tsx +41 -0
- package/src/components/watch.tsx +155 -0
- package/src/hooks/useApi.ts +183 -0
- package/src/positronic.ts +40 -0
- package/src/test/data/resources/config.json +1 -0
- package/src/test/data/resources/data/config.json +1 -0
- package/src/test/data/resources/data/logo.png +2 -0
- package/src/test/data/resources/docs/api.md +3 -0
- package/src/test/data/resources/docs/readme.md +3 -0
- package/src/test/data/resources/example.md +3 -0
- package/src/test/data/resources/file with spaces.txt +1 -0
- package/src/test/data/resources/readme.md +3 -0
- package/src/test/data/resources/test.txt +1 -0
- package/src/test/mock-api-client.ts +145 -0
- package/src/test/test-dev-server.ts +1003 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface Project {
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
addedAt: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProjectConfig {
|
|
12
|
+
version: string;
|
|
13
|
+
currentProject: string | null;
|
|
14
|
+
projects: Project[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ProjectConfigManager {
|
|
18
|
+
private configPath: string;
|
|
19
|
+
private configDir: string;
|
|
20
|
+
|
|
21
|
+
constructor(customConfigDir?: string) {
|
|
22
|
+
// Priority: customConfigDir > env variable > default home directory
|
|
23
|
+
this.configDir =
|
|
24
|
+
customConfigDir ||
|
|
25
|
+
process.env.POSITRONIC_CONFIG_DIR ||
|
|
26
|
+
path.join(os.homedir(), '.positronic');
|
|
27
|
+
this.configPath = path.join(this.configDir, 'config.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private ensureConfigDir(): void {
|
|
31
|
+
if (!fs.existsSync(this.configDir)) {
|
|
32
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private getDefaultConfig(): ProjectConfig {
|
|
37
|
+
return {
|
|
38
|
+
version: '1',
|
|
39
|
+
currentProject: null,
|
|
40
|
+
projects: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
read(): ProjectConfig {
|
|
45
|
+
this.ensureConfigDir();
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(this.configPath)) {
|
|
48
|
+
const defaultConfig = this.getDefaultConfig();
|
|
49
|
+
this.write(defaultConfig);
|
|
50
|
+
return defaultConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
55
|
+
return JSON.parse(content) as ProjectConfig;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error reading config file:', error);
|
|
58
|
+
// Return default config if file is corrupted
|
|
59
|
+
return this.getDefaultConfig();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
write(config: ProjectConfig): void {
|
|
64
|
+
this.ensureConfigDir();
|
|
65
|
+
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addProject(name: string, url: string): { success: boolean; error?: string } {
|
|
69
|
+
const config = this.read();
|
|
70
|
+
|
|
71
|
+
// Check for duplicate names
|
|
72
|
+
if (config.projects.some((p) => p.name === name)) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: `A project named "${name}" already exists`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate URL
|
|
80
|
+
try {
|
|
81
|
+
new URL(url);
|
|
82
|
+
} catch {
|
|
83
|
+
return { success: false, error: 'Invalid URL format' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const newProject: Project = {
|
|
87
|
+
name,
|
|
88
|
+
url,
|
|
89
|
+
addedAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
config.projects.push(newProject);
|
|
93
|
+
|
|
94
|
+
// If this is the first project, make it current
|
|
95
|
+
if (config.projects.length === 1) {
|
|
96
|
+
config.currentProject = name;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.write(config);
|
|
100
|
+
return { success: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
selectProject(name: string): { success: boolean; error?: string } {
|
|
104
|
+
const config = this.read();
|
|
105
|
+
|
|
106
|
+
const project = config.projects.find((p) => p.name === name);
|
|
107
|
+
if (!project) {
|
|
108
|
+
return { success: false, error: `Project "${name}" not found` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
config.currentProject = name;
|
|
112
|
+
this.write(config);
|
|
113
|
+
return { success: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
removeProject(name: string): { success: boolean; error?: string } {
|
|
117
|
+
const config = this.read();
|
|
118
|
+
|
|
119
|
+
const projectIndex = config.projects.findIndex((p) => p.name === name);
|
|
120
|
+
if (projectIndex === -1) {
|
|
121
|
+
return { success: false, error: `Project "${name}" not found` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
config.projects.splice(projectIndex, 1);
|
|
125
|
+
|
|
126
|
+
// If we removed the current project, clear it
|
|
127
|
+
if (config.currentProject === name) {
|
|
128
|
+
config.currentProject =
|
|
129
|
+
config.projects.length > 0 ? config.projects[0].name : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.write(config);
|
|
133
|
+
return { success: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getCurrentProject(): Project | null {
|
|
137
|
+
const config = this.read();
|
|
138
|
+
if (!config.currentProject) return null;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
config.projects.find((p) => p.name === config.currentProject) || null
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
listProjects(): { projects: Project[]; current: string | null } {
|
|
146
|
+
const config = this.read();
|
|
147
|
+
return {
|
|
148
|
+
projects: config.projects,
|
|
149
|
+
current: config.currentProject,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Integration Tests - Testing Philosophy
|
|
3
|
+
*
|
|
4
|
+
* These tests are designed to be maintainable and resilient to UI changes.
|
|
5
|
+
*
|
|
6
|
+
* Key principles:
|
|
7
|
+
* 1. **Test behavior, not formatting** - Don't check for specific icons, emojis, or exact formatting
|
|
8
|
+
* 2. **Look for essential keywords only** - Focus on the minimum text that indicates success/failure
|
|
9
|
+
* 3. **Use simple assertions** - Prefer toContain() over complex regex when possible
|
|
10
|
+
* 4. **Be case-insensitive** - Use toLowerCase() to avoid breaking on capitalization changes
|
|
11
|
+
*
|
|
12
|
+
* Examples:
|
|
13
|
+
* - For success: look for "added", "switched", etc. (not specific success emojis)
|
|
14
|
+
* - For errors: look for "not found", "invalid", "already exists" (not error icons)
|
|
15
|
+
* - For data: check that project names and URLs appear, but not their exact formatting
|
|
16
|
+
*
|
|
17
|
+
* This approach ensures tests remain stable as the CLI's output formatting evolves,
|
|
18
|
+
* while still verifying that core functionality works correctly.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Import nock to use in tests, but configuration happens in jest.setup.js
|
|
22
|
+
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import * as os from 'os';
|
|
26
|
+
import { describe, it, expect, afterEach, jest } from '@jest/globals';
|
|
27
|
+
import { createTestEnv, px } from './test-utils.js';
|
|
28
|
+
|
|
29
|
+
describe('CLI Integration: positronic server with project', () => {
|
|
30
|
+
it('runs a brain', async () => {
|
|
31
|
+
const env = await createTestEnv();
|
|
32
|
+
|
|
33
|
+
// Setup test brain
|
|
34
|
+
env.setup((dir: string) => {
|
|
35
|
+
const brainsDir = path.join(dir, 'brains');
|
|
36
|
+
fs.mkdirSync(brainsDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// Create a simple test brain
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(brainsDir, 'test-brain.ts'),
|
|
41
|
+
`
|
|
42
|
+
export default function testBrain() {
|
|
43
|
+
return {
|
|
44
|
+
title: 'Test Brain',
|
|
45
|
+
steps: [
|
|
46
|
+
{
|
|
47
|
+
title: 'Test Step',
|
|
48
|
+
run: async () => {
|
|
49
|
+
return { success: true };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const px = await env.start();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const { waitForOutput } = await px(['run', 'test-brain']);
|
|
63
|
+
|
|
64
|
+
// Verify the run command connected to the server and got a run ID
|
|
65
|
+
const outputContainsRunId = await waitForOutput(/Run ID:/);
|
|
66
|
+
const outputContainsRunPrefix = await waitForOutput(/run-/);
|
|
67
|
+
|
|
68
|
+
expect(outputContainsRunId).toBe(true);
|
|
69
|
+
expect(outputContainsRunPrefix).toBe(true);
|
|
70
|
+
} finally {
|
|
71
|
+
await env.stopAndCleanup();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('CLI Integration: project commands', () => {
|
|
77
|
+
let tempDir: string;
|
|
78
|
+
let configDir: string;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
// Create a temp directory for testing
|
|
82
|
+
tempDir = fs.mkdtempSync(
|
|
83
|
+
path.join(os.tmpdir(), 'positronic-project-test-')
|
|
84
|
+
);
|
|
85
|
+
configDir = path.join(tempDir, '.positronic');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
// Clean up test directory
|
|
90
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('project add', () => {
|
|
94
|
+
it('should add a new project successfully', async () => {
|
|
95
|
+
const { waitForOutput, instance } = await px(
|
|
96
|
+
['project', 'add', 'My App', '--url', 'https://my-app.positronic.sh'],
|
|
97
|
+
{ configDir }
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const isReady = await waitForOutput(/added/i);
|
|
101
|
+
expect(isReady).toBe(true);
|
|
102
|
+
const output = instance.lastFrame();
|
|
103
|
+
expect(output).toMatch(/my app/i);
|
|
104
|
+
expect(output).toContain('https://my-app.positronic.sh');
|
|
105
|
+
|
|
106
|
+
// Verify config file was created
|
|
107
|
+
const configPath = path.join(configDir, 'config.json');
|
|
108
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
109
|
+
|
|
110
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
111
|
+
expect(config.projects).toHaveLength(1);
|
|
112
|
+
expect(config.projects[0].name).toBe('My App');
|
|
113
|
+
expect(config.currentProject).toBe('My App');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should reject duplicate project names', async () => {
|
|
117
|
+
// Add first project
|
|
118
|
+
const addFirstProject = await px(
|
|
119
|
+
['project', 'add', 'My App', '--url', 'https://my-app.positronic.sh'],
|
|
120
|
+
{ configDir }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(await addFirstProject.waitForOutput(/added/i)).toBe(true);
|
|
124
|
+
|
|
125
|
+
// Try to add duplicate
|
|
126
|
+
const addDuplicateProject = await px(
|
|
127
|
+
['project', 'add', 'My App', '--url', 'https://other.positronic.sh'],
|
|
128
|
+
{ configDir }
|
|
129
|
+
);
|
|
130
|
+
expect(await addDuplicateProject.waitForOutput(/already exists/i)).toBe(
|
|
131
|
+
true
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should reject invalid URLs', async () => {
|
|
136
|
+
const { waitForOutput } = await px(
|
|
137
|
+
['project', 'add', 'My App', '--url', 'not-a-valid-url'],
|
|
138
|
+
{ configDir }
|
|
139
|
+
);
|
|
140
|
+
expect(await waitForOutput(/invalid/i)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle project names with spaces', async () => {
|
|
144
|
+
const { waitForOutput, instance } = await px(
|
|
145
|
+
[
|
|
146
|
+
'project',
|
|
147
|
+
'add',
|
|
148
|
+
'My Production App',
|
|
149
|
+
'--url',
|
|
150
|
+
'https://prod.positronic.sh',
|
|
151
|
+
],
|
|
152
|
+
{ configDir }
|
|
153
|
+
);
|
|
154
|
+
expect(await waitForOutput(/added/i)).toBe(true);
|
|
155
|
+
expect(instance.lastFrame()).toMatch(/my production app/i);
|
|
156
|
+
|
|
157
|
+
const config = JSON.parse(
|
|
158
|
+
fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
|
|
159
|
+
);
|
|
160
|
+
expect(config.projects[0].name).toBe('My Production App');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('project list', () => {
|
|
165
|
+
it('should show empty state when no projects configured', async () => {
|
|
166
|
+
const { instance } = await px(['project', 'list'], { configDir });
|
|
167
|
+
const output = instance.lastFrame() || '';
|
|
168
|
+
|
|
169
|
+
expect(output.toLowerCase()).toContain('no projects');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should list all projects with current indicator', async () => {
|
|
173
|
+
// Add some projects
|
|
174
|
+
await px(
|
|
175
|
+
['project', 'add', 'Project One', '--url', 'https://one.positronic.sh'],
|
|
176
|
+
{ configDir }
|
|
177
|
+
);
|
|
178
|
+
await px(
|
|
179
|
+
['project', 'add', 'Project Two', '--url', 'https://two.positronic.sh'],
|
|
180
|
+
{ configDir }
|
|
181
|
+
);
|
|
182
|
+
await px(['project', 'select', 'Project Two'], { configDir });
|
|
183
|
+
|
|
184
|
+
const { instance } = await px(['project', 'list'], { configDir });
|
|
185
|
+
const output = instance.lastFrame() || '';
|
|
186
|
+
|
|
187
|
+
expect(output).toMatch(/project one/i);
|
|
188
|
+
expect(output).toMatch(/project two/i);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('project select', () => {
|
|
193
|
+
beforeEach(async () => {
|
|
194
|
+
// Add some projects for selection tests
|
|
195
|
+
await px(
|
|
196
|
+
[
|
|
197
|
+
'project',
|
|
198
|
+
'add',
|
|
199
|
+
'Project Alpha',
|
|
200
|
+
'--url',
|
|
201
|
+
'https://alpha.positronic.sh',
|
|
202
|
+
],
|
|
203
|
+
{ configDir }
|
|
204
|
+
);
|
|
205
|
+
await px(
|
|
206
|
+
[
|
|
207
|
+
'project',
|
|
208
|
+
'add',
|
|
209
|
+
'Project Beta',
|
|
210
|
+
'--url',
|
|
211
|
+
'https://beta.positronic.sh',
|
|
212
|
+
],
|
|
213
|
+
{ configDir }
|
|
214
|
+
);
|
|
215
|
+
await px(
|
|
216
|
+
[
|
|
217
|
+
'project',
|
|
218
|
+
'add',
|
|
219
|
+
'Project Gamma',
|
|
220
|
+
'--url',
|
|
221
|
+
'https://gamma.positronic.sh',
|
|
222
|
+
],
|
|
223
|
+
{ configDir }
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should select a project by name', async () => {
|
|
228
|
+
const { instance } = await px(['project', 'select', 'Project Beta'], {
|
|
229
|
+
configDir,
|
|
230
|
+
});
|
|
231
|
+
const output = instance.lastFrame() || '';
|
|
232
|
+
|
|
233
|
+
expect(output.toLowerCase()).toMatch(/switched|selected/);
|
|
234
|
+
expect(output).toMatch(/project beta/i);
|
|
235
|
+
|
|
236
|
+
// Verify config was updated
|
|
237
|
+
const config = JSON.parse(
|
|
238
|
+
fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
|
|
239
|
+
);
|
|
240
|
+
expect(config.currentProject).toBe('Project Beta');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should show error for non-existent project', async () => {
|
|
244
|
+
const { instance } = await px(['project', 'select', 'Non Existent'], {
|
|
245
|
+
configDir,
|
|
246
|
+
});
|
|
247
|
+
const output = instance.lastFrame() || '';
|
|
248
|
+
|
|
249
|
+
expect(output.toLowerCase()).toContain('not found');
|
|
250
|
+
expect(output).toMatch(/project alpha/i);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should show interactive selection when no name provided', async () => {
|
|
254
|
+
const { instance } = await px(['project', 'select'], { configDir });
|
|
255
|
+
const output = instance.lastFrame() || '';
|
|
256
|
+
|
|
257
|
+
// In test environment, raw mode isn't supported so it shows a non-interactive list
|
|
258
|
+
expect(output).toMatch(/project alpha/i);
|
|
259
|
+
expect(output).toMatch(/project beta/i);
|
|
260
|
+
expect(output).toMatch(/project gamma/i);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('project show', () => {
|
|
265
|
+
it('should show no project selected when empty', async () => {
|
|
266
|
+
const { instance } = await px(['project', 'show'], { configDir });
|
|
267
|
+
const output = instance.lastFrame() || '';
|
|
268
|
+
|
|
269
|
+
expect(output.toLowerCase()).toContain('no project');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should show current project details', async () => {
|
|
273
|
+
await px(
|
|
274
|
+
[
|
|
275
|
+
'project',
|
|
276
|
+
'add',
|
|
277
|
+
'My Current Project',
|
|
278
|
+
'--url',
|
|
279
|
+
'https://current.positronic.sh',
|
|
280
|
+
],
|
|
281
|
+
{ configDir }
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const { instance } = await px(['project', 'show'], { configDir });
|
|
285
|
+
const output = instance.lastFrame() || '';
|
|
286
|
+
|
|
287
|
+
expect(output).toMatch(/my current project/i);
|
|
288
|
+
expect(output).toContain('https://current.positronic.sh');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should show other projects count when multiple exist', async () => {
|
|
292
|
+
await px(
|
|
293
|
+
['project', 'add', 'Project 1', '--url', 'https://one.positronic.sh'],
|
|
294
|
+
{ configDir }
|
|
295
|
+
);
|
|
296
|
+
await px(
|
|
297
|
+
['project', 'add', 'Project 2', '--url', 'https://two.positronic.sh'],
|
|
298
|
+
{ configDir }
|
|
299
|
+
);
|
|
300
|
+
await px(
|
|
301
|
+
['project', 'add', 'Project 3', '--url', 'https://three.positronic.sh'],
|
|
302
|
+
{ configDir }
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const { instance } = await px(['project', 'show'], { configDir });
|
|
306
|
+
const output = instance.lastFrame() || '';
|
|
307
|
+
|
|
308
|
+
expect(output.toLowerCase()).toContain('2 other');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('project rm', () => {
|
|
313
|
+
beforeEach(async () => {
|
|
314
|
+
// Add some projects for removal tests
|
|
315
|
+
await px(
|
|
316
|
+
['project', 'add', 'Project A', '--url', 'https://a.positronic.sh'],
|
|
317
|
+
{ configDir }
|
|
318
|
+
);
|
|
319
|
+
await px(
|
|
320
|
+
['project', 'add', 'Project B', '--url', 'https://b.positronic.sh'],
|
|
321
|
+
{ configDir }
|
|
322
|
+
);
|
|
323
|
+
await px(
|
|
324
|
+
['project', 'add', 'Project C', '--url', 'https://c.positronic.sh'],
|
|
325
|
+
{ configDir }
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should remove a project successfully', async () => {
|
|
330
|
+
const { waitForOutput, instance } = await px(
|
|
331
|
+
['project', 'rm', 'Project B'],
|
|
332
|
+
{
|
|
333
|
+
configDir,
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const isReady = await waitForOutput(/removed successfully/i);
|
|
338
|
+
expect(isReady).toBe(true);
|
|
339
|
+
|
|
340
|
+
const output = instance.lastFrame() || '';
|
|
341
|
+
expect(output).toMatch(/project b/i);
|
|
342
|
+
|
|
343
|
+
// Verify project was removed from config
|
|
344
|
+
const config = JSON.parse(
|
|
345
|
+
fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
|
|
346
|
+
);
|
|
347
|
+
expect(config.projects).toHaveLength(2);
|
|
348
|
+
expect(
|
|
349
|
+
config.projects.find((p: any) => p.name === 'Project B')
|
|
350
|
+
).toBeUndefined();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should handle removing the current project', async () => {
|
|
354
|
+
// Select Project B as current
|
|
355
|
+
await px(['project', 'select', 'Project B'], { configDir });
|
|
356
|
+
|
|
357
|
+
// Remove the current project
|
|
358
|
+
const { waitForOutput, instance } = await px(
|
|
359
|
+
['project', 'rm', 'Project B'],
|
|
360
|
+
{
|
|
361
|
+
configDir,
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const isReady = await waitForOutput(/removed successfully/i);
|
|
366
|
+
expect(isReady).toBe(true);
|
|
367
|
+
|
|
368
|
+
const output = instance.lastFrame() || '';
|
|
369
|
+
expect(output).toMatch(/project b/i);
|
|
370
|
+
|
|
371
|
+
// Verify current project was switched to another project
|
|
372
|
+
const config = JSON.parse(
|
|
373
|
+
fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
|
|
374
|
+
);
|
|
375
|
+
expect(config.currentProject).not.toBe('Project B');
|
|
376
|
+
expect(config.currentProject).toBeTruthy(); // Should be either Project A or Project C
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should handle removing the last project', async () => {
|
|
380
|
+
// Remove all but one project
|
|
381
|
+
await px(['project', 'rm', 'Project A'], { configDir });
|
|
382
|
+
await px(['project', 'rm', 'Project B'], { configDir });
|
|
383
|
+
|
|
384
|
+
// Remove the last project
|
|
385
|
+
const { waitForOutput, instance } = await px(
|
|
386
|
+
['project', 'rm', 'Project C'],
|
|
387
|
+
{
|
|
388
|
+
configDir,
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const isReady = await waitForOutput(/removed successfully/i);
|
|
393
|
+
expect(isReady).toBe(true);
|
|
394
|
+
|
|
395
|
+
const output = instance.lastFrame() || '';
|
|
396
|
+
expect(output.toLowerCase()).toMatch(/no active project/);
|
|
397
|
+
|
|
398
|
+
// Verify no projects remain and current project is null
|
|
399
|
+
const config = JSON.parse(
|
|
400
|
+
fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8')
|
|
401
|
+
);
|
|
402
|
+
expect(config.projects).toHaveLength(0);
|
|
403
|
+
expect(config.currentProject).toBeNull();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should show error for non-existent project', async () => {
|
|
407
|
+
const { waitForOutput, instance } = await px(
|
|
408
|
+
['project', 'rm', 'Non Existent'],
|
|
409
|
+
{
|
|
410
|
+
configDir,
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const isReady = await waitForOutput(/failed to remove/i);
|
|
415
|
+
expect(isReady).toBe(true);
|
|
416
|
+
|
|
417
|
+
const output = instance.lastFrame() || '';
|
|
418
|
+
expect(output.toLowerCase()).toMatch(/not found/);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('project command interactions', () => {
|
|
423
|
+
it('should maintain state across commands', async () => {
|
|
424
|
+
// Add multiple projects
|
|
425
|
+
await px(
|
|
426
|
+
['project', 'add', 'First', '--url', 'https://first.positronic.sh'],
|
|
427
|
+
{ configDir }
|
|
428
|
+
);
|
|
429
|
+
await px(
|
|
430
|
+
['project', 'add', 'Second', '--url', 'https://second.positronic.sh'],
|
|
431
|
+
{ configDir }
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
let { instance } = await px(['project', 'show'], { configDir });
|
|
435
|
+
let output = instance.lastFrame() || '';
|
|
436
|
+
expect(output).toMatch(/first/i);
|
|
437
|
+
|
|
438
|
+
// Switch projects
|
|
439
|
+
await px(['project', 'select', 'Second'], { configDir });
|
|
440
|
+
|
|
441
|
+
// Verify switch worked
|
|
442
|
+
({ instance } = await px(['project', 'show'], { configDir }));
|
|
443
|
+
output = instance.lastFrame() || '';
|
|
444
|
+
expect(output).toMatch(/second/i);
|
|
445
|
+
|
|
446
|
+
// List should show second project
|
|
447
|
+
({ instance } = await px(['project', 'list'], { configDir }));
|
|
448
|
+
output = instance.lastFrame() || '';
|
|
449
|
+
expect(output).toMatch(/second/i);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('CLI Integration: project new', () => {
|
|
454
|
+
let tmpRoot: string;
|
|
455
|
+
let originalLocalPath: string | undefined;
|
|
456
|
+
|
|
457
|
+
beforeEach(() => {
|
|
458
|
+
// Set POSITRONIC_LOCAL_PATH to use local template instead of npm
|
|
459
|
+
originalLocalPath = process.env.POSITRONIC_LOCAL_PATH;
|
|
460
|
+
process.env.POSITRONIC_LOCAL_PATH = path.resolve('.');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
afterEach(() => {
|
|
464
|
+
// Restore original environment
|
|
465
|
+
if (originalLocalPath) {
|
|
466
|
+
process.env.POSITRONIC_LOCAL_PATH = originalLocalPath;
|
|
467
|
+
} else {
|
|
468
|
+
delete process.env.POSITRONIC_LOCAL_PATH;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (tmpRoot && fs.existsSync(tmpRoot)) {
|
|
472
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should create a new project directory and output success message', async () => {
|
|
477
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'positronic-new-test-'));
|
|
478
|
+
const projectDir = path.join(tmpRoot, 'my-new-project');
|
|
479
|
+
|
|
480
|
+
const { waitForOutput, instance } = await px([
|
|
481
|
+
'project',
|
|
482
|
+
'new',
|
|
483
|
+
projectDir,
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
// Wait for success text from the UI component
|
|
487
|
+
const isReady = await waitForOutput(/project created successfully/i, 200);
|
|
488
|
+
expect(isReady).toBe(true);
|
|
489
|
+
|
|
490
|
+
// Validate CLI output contains the project name
|
|
491
|
+
const output = instance.lastFrame() || '';
|
|
492
|
+
expect(output.toLowerCase()).toContain('my-new-project');
|
|
493
|
+
|
|
494
|
+
// Ensure project directory and essential files exist
|
|
495
|
+
expect(fs.existsSync(projectDir)).toBe(true);
|
|
496
|
+
expect(
|
|
497
|
+
fs.existsSync(path.join(projectDir, 'positronic.config.json'))
|
|
498
|
+
).toBe(true);
|
|
499
|
+
expect(fs.existsSync(path.join(projectDir, 'package.json'))).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|