@vxnus/siduri 0.0.5 → 0.0.7
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/README.md +47 -28
- package/dist/builtin-manifests.d.ts +2 -0
- package/dist/builtin-manifests.js +433 -0
- package/dist/clean-machine-e2e.test.d.ts +1 -0
- package/dist/clean-machine-e2e.test.js +222 -0
- package/dist/db.d.ts +25 -0
- package/dist/db.js +152 -0
- package/dist/discovery.d.ts +13 -0
- package/dist/discovery.js +87 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +67 -0
- package/dist/doctor-db.test.d.ts +1 -0
- package/dist/doctor-db.test.js +135 -0
- package/dist/doctor.d.ts +19 -0
- package/dist/doctor.js +225 -0
- package/dist/generator.d.ts +18 -0
- package/dist/generator.js +215 -0
- package/dist/generator.test.d.ts +1 -0
- package/dist/generator.test.js +158 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +202 -360
- package/dist/manifest.d.ts +33 -0
- package/dist/manifest.js +40 -0
- package/dist/release-check.d.ts +9 -0
- package/dist/release-check.js +131 -0
- package/package.json +12 -11
- package/dist/runtime.js +0 -3299
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const generator_1 = require("./generator");
|
|
10
|
+
const discovery_1 = require("./discovery");
|
|
11
|
+
describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
|
|
12
|
+
const repoRoot = node_path_1.default.resolve(__dirname, '../..');
|
|
13
|
+
const tempPackDir = node_path_1.default.resolve(__dirname, '../temp-packs-e2e');
|
|
14
|
+
const cleanMachineRoot = node_path_1.default.resolve(__dirname, '../temp-clean-machine-e2e');
|
|
15
|
+
const ALL_CANONICAL_PACKAGES = [
|
|
16
|
+
{ filter: '@siduri-x/core', tarName: 'siduri-x-core-1.0.0.tgz', isOrgan: false },
|
|
17
|
+
{ filter: '@siduri-x/brain', tarName: 'siduri-x-brain-1.0.0.tgz', isOrgan: true },
|
|
18
|
+
{ filter: '@siduri-x/memory', tarName: 'siduri-x-memory-1.0.0.tgz', isOrgan: true },
|
|
19
|
+
{ filter: '@siduri-x/knowledge', tarName: 'siduri-x-knowledge-1.0.0.tgz', isOrgan: true },
|
|
20
|
+
{ filter: '@siduri-x/behavior', tarName: 'siduri-x-behavior-1.0.0.tgz', isOrgan: true },
|
|
21
|
+
{ filter: '@siduri-x/ear', tarName: 'siduri-x-ear-1.0.0.tgz', isOrgan: true },
|
|
22
|
+
{ filter: '@siduri-x/vision', tarName: 'siduri-x-vision-1.0.0.tgz', isOrgan: true },
|
|
23
|
+
{ filter: '@siduri-x/hands', tarName: 'siduri-x-hands-1.0.0.tgz', isOrgan: true },
|
|
24
|
+
{ filter: '@siduri-x/body', tarName: 'siduri-x-body-1.0.0.tgz', isOrgan: true },
|
|
25
|
+
{ filter: '@siduri-x/voice', tarName: 'siduri-x-voice-1.0.0.tgz', isOrgan: true },
|
|
26
|
+
{ filter: '@siduri-x/observation', tarName: 'siduri-x-observation-1.0.0.tgz', isOrgan: true },
|
|
27
|
+
{ filter: '@vxnus/siduri', tarName: 'vxnus-siduri-0.0.7.tgz', isOrgan: false },
|
|
28
|
+
];
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
// 1. Prepare clean directories
|
|
31
|
+
if (node_fs_1.default.existsSync(tempPackDir))
|
|
32
|
+
node_fs_1.default.rmSync(tempPackDir, { recursive: true, force: true });
|
|
33
|
+
if (node_fs_1.default.existsSync(cleanMachineRoot))
|
|
34
|
+
node_fs_1.default.rmSync(cleanMachineRoot, { recursive: true, force: true });
|
|
35
|
+
node_fs_1.default.mkdirSync(tempPackDir, { recursive: true });
|
|
36
|
+
node_fs_1.default.mkdirSync(cleanMachineRoot, { recursive: true });
|
|
37
|
+
// 2. Build all packages in repo
|
|
38
|
+
(0, node_child_process_1.execSync)('pnpm build', { cwd: repoRoot, stdio: 'pipe' });
|
|
39
|
+
// 3. Pack each canonical package into tempPackDir
|
|
40
|
+
for (const pkg of ALL_CANONICAL_PACKAGES) {
|
|
41
|
+
(0, node_child_process_1.execSync)(`pnpm --filter ${pkg.filter} pack --pack-destination ${tempPackDir}`, {
|
|
42
|
+
cwd: repoRoot,
|
|
43
|
+
stdio: 'pipe',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}, 90000);
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
if (node_fs_1.default.existsSync(tempPackDir))
|
|
49
|
+
node_fs_1.default.rmSync(tempPackDir, { recursive: true, force: true });
|
|
50
|
+
if (node_fs_1.default.existsSync(cleanMachineRoot))
|
|
51
|
+
node_fs_1.default.rmSync(cleanMachineRoot, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
describe('Phase 5A: Package Artifact Verification', () => {
|
|
54
|
+
test('all 12 packages produce valid tarballs', () => {
|
|
55
|
+
for (const pkg of ALL_CANONICAL_PACKAGES) {
|
|
56
|
+
const tarPath = node_path_1.default.join(tempPackDir, pkg.tarName);
|
|
57
|
+
expect(node_fs_1.default.existsSync(tarPath)).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
test('packed package.json files have zero workspace:* or link: dependencies', () => {
|
|
61
|
+
for (const pkg of ALL_CANONICAL_PACKAGES) {
|
|
62
|
+
const tarPath = node_path_1.default.join(tempPackDir, pkg.tarName);
|
|
63
|
+
const pkgJsonRaw = (0, node_child_process_1.execSync)(`tar -xzf ${tarPath} -O package/package.json`, { encoding: 'utf8' });
|
|
64
|
+
const pkgJson = JSON.parse(pkgJsonRaw);
|
|
65
|
+
const allDeps = {
|
|
66
|
+
...(pkgJson.dependencies || {}),
|
|
67
|
+
...(pkgJson.peerDependencies || {}),
|
|
68
|
+
};
|
|
69
|
+
for (const [depName, version] of Object.entries(allDeps)) {
|
|
70
|
+
if (typeof version === 'string') {
|
|
71
|
+
expect(version.startsWith('workspace:')).toBe(false);
|
|
72
|
+
expect(version.startsWith('link:')).toBe(false);
|
|
73
|
+
expect(version.includes('../')).toBe(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
test('every organ package tarball contains organ-manifest.json and dist files', () => {
|
|
79
|
+
const organPackages = ALL_CANONICAL_PACKAGES.filter((p) => p.isOrgan);
|
|
80
|
+
for (const pkg of organPackages) {
|
|
81
|
+
const tarPath = node_path_1.default.join(tempPackDir, pkg.tarName);
|
|
82
|
+
const listing = (0, node_child_process_1.execSync)(`tar -tzf ${tarPath}`, { encoding: 'utf8' }).split('\n');
|
|
83
|
+
expect(listing.some((line) => line.includes('package/organ-manifest.json'))).toBe(true);
|
|
84
|
+
expect(listing.some((line) => line.includes('package/dist/index.js'))).toBe(true);
|
|
85
|
+
expect(listing.some((line) => line.includes('package/dist/index.d.ts'))).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
test('memory organ tarball packages migrations/001_initial_schema.sql', () => {
|
|
89
|
+
const memoryTarPath = node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.0.tgz');
|
|
90
|
+
const listing = (0, node_child_process_1.execSync)(`tar -tzf ${memoryTarPath}`, { encoding: 'utf8' });
|
|
91
|
+
expect(listing).toContain('package/migrations/001_initial_schema.sql');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('Phase 5B & 5C & 5G: Clean-Machine Installation & Isolation Acceptance', () => {
|
|
95
|
+
const registry = discovery_1.OrganRegistry.discover([node_path_1.default.resolve(repoRoot, 'packages/organs')]);
|
|
96
|
+
const brain = registry.get('brain');
|
|
97
|
+
const hands = registry.get('hands');
|
|
98
|
+
const memory = registry.get('memory');
|
|
99
|
+
test('Clean Composition 1: Brain only instance installs, starts, runs doctor and db push', () => {
|
|
100
|
+
const instanceDir = node_path_1.default.join(cleanMachineRoot, 'inst-brain-only');
|
|
101
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(instanceDir, 'src'), { recursive: true });
|
|
102
|
+
const files = (0, generator_1.generateInstanceFiles)({
|
|
103
|
+
name: 'CleanBrainOnly',
|
|
104
|
+
selectedManifests: [brain],
|
|
105
|
+
});
|
|
106
|
+
// Write instance files referencing packed tarballs directly for true clean machine install
|
|
107
|
+
const pkgObj = JSON.parse(files['package.json']);
|
|
108
|
+
pkgObj.dependencies = {
|
|
109
|
+
'@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.0.tgz')}`,
|
|
110
|
+
'@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.0.tgz')}`,
|
|
111
|
+
};
|
|
112
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
|
|
113
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
|
|
114
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.schema.json'), files['siduri.schema.json']);
|
|
115
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, '.env.example'), files['.env.example']);
|
|
116
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'README.md'), files['README.md']);
|
|
117
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'src/index.js'), files['src/index.js']);
|
|
118
|
+
// 1. Run npm install in isolated directory
|
|
119
|
+
(0, node_child_process_1.execSync)('npm install --no-audit --no-fund', { cwd: instanceDir, stdio: 'pipe' });
|
|
120
|
+
// 2. Assert unselected organs are NOT in node_modules
|
|
121
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/memory'))).toBe(false);
|
|
122
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/hands'))).toBe(false);
|
|
123
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/voice'))).toBe(false);
|
|
124
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'siduri-x-runtime.js'))).toBe(false);
|
|
125
|
+
// 3. Run node src/index.js (starts cleanly without PostgreSQL or unselected organs)
|
|
126
|
+
const startOutput = (0, node_child_process_1.execSync)('node src/index.js', { cwd: instanceDir, encoding: 'utf8' });
|
|
127
|
+
expect(startOutput).toContain('✓ Siduri [CleanBrainOnly] initialized with [Brain].');
|
|
128
|
+
// 4. Verify doctor from standalone directory
|
|
129
|
+
const doctorPass = (0, node_child_process_1.execSync)(`node ${node_path_1.default.resolve(repoRoot, 'cli/dist/index.js')} doctor`, {
|
|
130
|
+
cwd: instanceDir,
|
|
131
|
+
env: { ...process.env, OPENROUTER_API_KEY: 'test-key-clean' },
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
});
|
|
134
|
+
expect(doctorPass).toContain('Result: PASS');
|
|
135
|
+
expect(doctorPass).toContain('Database');
|
|
136
|
+
expect(doctorPass).toContain('Not required by current composition');
|
|
137
|
+
// 5. Verify db push from standalone directory
|
|
138
|
+
const dbPushOutput = (0, node_child_process_1.execSync)(`node ${node_path_1.default.resolve(repoRoot, 'cli/dist/index.js')} db push`, {
|
|
139
|
+
cwd: instanceDir,
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
});
|
|
142
|
+
expect(dbPushOutput).toContain('No database migrations are required by this instance.');
|
|
143
|
+
}, 60000);
|
|
144
|
+
test('Clean Composition 2: Brain + Hands installs and runs doctor verifying ACTION_POLICY_SECRET', () => {
|
|
145
|
+
const instanceDir = node_path_1.default.join(cleanMachineRoot, 'inst-brain-hands');
|
|
146
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(instanceDir, 'src'), { recursive: true });
|
|
147
|
+
const files = (0, generator_1.generateInstanceFiles)({
|
|
148
|
+
name: 'CleanBrainHands',
|
|
149
|
+
selectedManifests: [brain, hands],
|
|
150
|
+
});
|
|
151
|
+
const pkgObj = JSON.parse(files['package.json']);
|
|
152
|
+
pkgObj.dependencies = {
|
|
153
|
+
'@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.0.tgz')}`,
|
|
154
|
+
'@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.0.tgz')}`,
|
|
155
|
+
'@siduri-x/hands': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-hands-1.0.0.tgz')}`,
|
|
156
|
+
};
|
|
157
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
|
|
158
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
|
|
159
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.schema.json'), files['siduri.schema.json']);
|
|
160
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, '.env.example'), files['.env.example']);
|
|
161
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'README.md'), files['README.md']);
|
|
162
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'src/index.js'), files['src/index.js']);
|
|
163
|
+
(0, node_child_process_1.execSync)('npm install --no-audit --no-fund', { cwd: instanceDir, stdio: 'pipe' });
|
|
164
|
+
expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/memory'))).toBe(false);
|
|
165
|
+
const startOutput = (0, node_child_process_1.execSync)('node src/index.js', { cwd: instanceDir, encoding: 'utf8' });
|
|
166
|
+
expect(startOutput).toContain('✓ Siduri [CleanBrainHands] initialized with [Brain, Hands].');
|
|
167
|
+
const doctorOutput = (0, node_child_process_1.execSync)(`node ${node_path_1.default.resolve(repoRoot, 'cli/dist/index.js')} doctor`, {
|
|
168
|
+
cwd: instanceDir,
|
|
169
|
+
env: {
|
|
170
|
+
...process.env,
|
|
171
|
+
OPENROUTER_API_KEY: 'test-key-clean',
|
|
172
|
+
ACTION_POLICY_SECRET: 'test-secret-clean',
|
|
173
|
+
},
|
|
174
|
+
encoding: 'utf8',
|
|
175
|
+
});
|
|
176
|
+
expect(doctorOutput).toContain('ACTION_POLICY_SECRET');
|
|
177
|
+
expect(doctorOutput).toContain('Result: PASS');
|
|
178
|
+
}, 60000);
|
|
179
|
+
test('Clean Composition 3: Brain + Memory discovers packaged migrations and detects DATABASE_URL', () => {
|
|
180
|
+
const instanceDir = node_path_1.default.join(cleanMachineRoot, 'inst-brain-memory');
|
|
181
|
+
node_fs_1.default.mkdirSync(node_path_1.default.join(instanceDir, 'src'), { recursive: true });
|
|
182
|
+
const files = (0, generator_1.generateInstanceFiles)({
|
|
183
|
+
name: 'CleanBrainMemory',
|
|
184
|
+
selectedManifests: [brain, memory],
|
|
185
|
+
});
|
|
186
|
+
const pkgObj = JSON.parse(files['package.json']);
|
|
187
|
+
pkgObj.dependencies = {
|
|
188
|
+
'@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.0.tgz')}`,
|
|
189
|
+
'@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.0.tgz')}`,
|
|
190
|
+
'@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.0.tgz')}`,
|
|
191
|
+
};
|
|
192
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
|
|
193
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
|
|
194
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.schema.json'), files['siduri.schema.json']);
|
|
195
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, '.env.example'), files['.env.example']);
|
|
196
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'README.md'), files['README.md']);
|
|
197
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'src/index.js'), files['src/index.js']);
|
|
198
|
+
(0, node_child_process_1.execSync)('npm install --no-audit --no-fund', { cwd: instanceDir, stdio: 'pipe' });
|
|
199
|
+
// Verify migrations exist inside node_modules/@siduri-x/memory/migrations
|
|
200
|
+
const installedMigrationsPath = node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/memory/migrations/001_initial_schema.sql');
|
|
201
|
+
expect(node_fs_1.default.existsSync(installedMigrationsPath)).toBe(true);
|
|
202
|
+
// Doctor detects database requirement
|
|
203
|
+
let doctorOutput = '';
|
|
204
|
+
try {
|
|
205
|
+
doctorOutput = (0, node_child_process_1.execSync)(`node ${node_path_1.default.resolve(repoRoot, 'cli/dist/index.js')} doctor`, {
|
|
206
|
+
cwd: instanceDir,
|
|
207
|
+
env: {
|
|
208
|
+
...process.env,
|
|
209
|
+
OPENROUTER_API_KEY: 'test-key-clean',
|
|
210
|
+
DATABASE_URL: '', // deliberately empty to test requirement detection
|
|
211
|
+
},
|
|
212
|
+
encoding: 'utf8',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
doctorOutput = (err.stdout || '') + (err.stderr || '');
|
|
217
|
+
}
|
|
218
|
+
expect(doctorOutput).toContain('DATABASE_URL is not configured');
|
|
219
|
+
expect(doctorOutput).toContain('Result: FAIL');
|
|
220
|
+
}, 60000);
|
|
221
|
+
});
|
|
222
|
+
});
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare function redactDatabaseUrl(url: string): string;
|
|
2
|
+
export interface MigrationFile {
|
|
3
|
+
name: string;
|
|
4
|
+
fullPath: string;
|
|
5
|
+
sql: string;
|
|
6
|
+
checksum: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MigrationRecord {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
checksum: string;
|
|
12
|
+
applied_at: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DbPushOptions {
|
|
15
|
+
projectDir?: string;
|
|
16
|
+
connectionString?: string;
|
|
17
|
+
env?: Record<string, string | undefined>;
|
|
18
|
+
}
|
|
19
|
+
export interface DbPushResult {
|
|
20
|
+
status: 'NOOP' | 'APPLIED' | 'UP_TO_DATE';
|
|
21
|
+
appliedMigrations: string[];
|
|
22
|
+
skippedMigrations: string[];
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function runDbPush(options?: DbPushOptions): Promise<DbPushResult>;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.redactDatabaseUrl = redactDatabaseUrl;
|
|
7
|
+
exports.runDbPush = runDbPush;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const pg_1 = require("pg");
|
|
12
|
+
const discovery_1 = require("./discovery");
|
|
13
|
+
function redactDatabaseUrl(url) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url);
|
|
16
|
+
if (parsed.password) {
|
|
17
|
+
parsed.password = '***';
|
|
18
|
+
}
|
|
19
|
+
return parsed.toString();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 'postgres://***:***@...';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function runDbPush(options = {}) {
|
|
26
|
+
const projectDir = options.projectDir ? node_path_1.default.resolve(options.projectDir) : process.cwd();
|
|
27
|
+
const configPath = node_path_1.default.join(projectDir, 'siduri.config.json');
|
|
28
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
29
|
+
throw new Error(`siduri.config.json not found at ${projectDir}. Make sure you are in a Siduri instance directory.`);
|
|
30
|
+
}
|
|
31
|
+
const config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
|
|
32
|
+
const configuredOrgans = config.organs || {};
|
|
33
|
+
// 1. Discover manifests of configured organs
|
|
34
|
+
const registry = discovery_1.OrganRegistry.discover([
|
|
35
|
+
node_path_1.default.join(projectDir, 'node_modules/@siduri-x'),
|
|
36
|
+
node_path_1.default.resolve(__dirname, '../../packages/organs'),
|
|
37
|
+
node_path_1.default.resolve(process.cwd(), 'packages/organs'),
|
|
38
|
+
]);
|
|
39
|
+
const databaseOrgans = [];
|
|
40
|
+
for (const [key] of Object.entries(configuredOrgans)) {
|
|
41
|
+
const manifest = registry.get(key) || registry.getAll().find((m) => m.configKey === key || m.organType === key);
|
|
42
|
+
if (manifest && manifest.database && manifest.database.migrationsDir) {
|
|
43
|
+
// Resolve migrations directory relative to the organ package
|
|
44
|
+
const candidatePaths = [
|
|
45
|
+
node_path_1.default.resolve(projectDir, 'node_modules', manifest.name, manifest.database.migrationsDir),
|
|
46
|
+
node_path_1.default.resolve(__dirname, '../../packages/organs', manifest.organType, manifest.database.migrationsDir),
|
|
47
|
+
node_path_1.default.resolve(process.cwd(), 'packages/organs', manifest.organType, manifest.database.migrationsDir),
|
|
48
|
+
];
|
|
49
|
+
const resolvedDir = candidatePaths.find((p) => node_fs_1.default.existsSync(p));
|
|
50
|
+
if (resolvedDir) {
|
|
51
|
+
databaseOrgans.push({
|
|
52
|
+
organType: manifest.organType,
|
|
53
|
+
manifest,
|
|
54
|
+
migrationsDir: resolvedDir,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (databaseOrgans.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'NOOP',
|
|
62
|
+
appliedMigrations: [],
|
|
63
|
+
skippedMigrations: [],
|
|
64
|
+
message: 'No database migrations are required by this instance.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// 2. Resolve DATABASE_URL
|
|
68
|
+
const connectionString = options.connectionString ||
|
|
69
|
+
options.env?.DATABASE_URL ||
|
|
70
|
+
process.env.DATABASE_URL;
|
|
71
|
+
if (!connectionString) {
|
|
72
|
+
throw new Error('DATABASE_URL is required to run database migrations. Set it in .env or environment.');
|
|
73
|
+
}
|
|
74
|
+
const pool = new pg_1.Pool({
|
|
75
|
+
connectionString,
|
|
76
|
+
connectionTimeoutMillis: 5000,
|
|
77
|
+
});
|
|
78
|
+
const client = await pool.connect();
|
|
79
|
+
try {
|
|
80
|
+
// 3. Ensure migrations table exists
|
|
81
|
+
await client.query(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS siduri_migrations (
|
|
83
|
+
id SERIAL PRIMARY KEY,
|
|
84
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
85
|
+
checksum VARCHAR(64) NOT NULL,
|
|
86
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
// 4. Fetch applied migrations
|
|
90
|
+
const existingRes = await client.query('SELECT name, checksum FROM siduri_migrations ORDER BY id ASC;');
|
|
91
|
+
const appliedMap = new Map();
|
|
92
|
+
for (const row of existingRes.rows) {
|
|
93
|
+
appliedMap.set(row.name, row.checksum);
|
|
94
|
+
}
|
|
95
|
+
// 5. Gather all migration files across database organs in deterministic order
|
|
96
|
+
const allMigrationFiles = [];
|
|
97
|
+
for (const item of databaseOrgans) {
|
|
98
|
+
const files = node_fs_1.default.readdirSync(item.migrationsDir).filter((f) => f.endsWith('.sql')).sort();
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const fullPath = node_path_1.default.join(item.migrationsDir, file);
|
|
101
|
+
const sql = node_fs_1.default.readFileSync(fullPath, 'utf8');
|
|
102
|
+
const checksum = (0, node_crypto_1.createHash)('sha256').update(sql).digest('hex');
|
|
103
|
+
allMigrationFiles.push({
|
|
104
|
+
name: `${item.organType}/${file}`,
|
|
105
|
+
fullPath,
|
|
106
|
+
sql,
|
|
107
|
+
checksum,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
allMigrationFiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
112
|
+
const appliedList = [];
|
|
113
|
+
const skippedList = [];
|
|
114
|
+
for (const migration of allMigrationFiles) {
|
|
115
|
+
const recordedChecksum = appliedMap.get(migration.name);
|
|
116
|
+
if (recordedChecksum !== undefined) {
|
|
117
|
+
if (recordedChecksum !== migration.checksum) {
|
|
118
|
+
throw new Error(`Migration checksum mismatch for ${migration.name}! Recorded: ${recordedChecksum.slice(0, 8)}, Current: ${migration.checksum.slice(0, 8)}. Refusing to apply modified migration.`);
|
|
119
|
+
}
|
|
120
|
+
skippedList.push(migration.name);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Apply migration in transaction
|
|
124
|
+
await client.query('BEGIN');
|
|
125
|
+
try {
|
|
126
|
+
await client.query(migration.sql);
|
|
127
|
+
await client.query('INSERT INTO siduri_migrations (name, checksum) VALUES ($1, $2)', [migration.name, migration.checksum]);
|
|
128
|
+
await client.query('COMMIT');
|
|
129
|
+
appliedList.push(migration.name);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
await client.query('ROLLBACK');
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const redacted = redactDatabaseUrl(connectionString);
|
|
138
|
+
const message = appliedList.length > 0
|
|
139
|
+
? `Applied ${appliedList.length} migration(s) to ${redacted}`
|
|
140
|
+
: `Database schema up to date on ${redacted}`;
|
|
141
|
+
return {
|
|
142
|
+
status: appliedList.length > 0 ? 'APPLIED' : 'UP_TO_DATE',
|
|
143
|
+
appliedMigrations: appliedList,
|
|
144
|
+
skippedMigrations: skippedList,
|
|
145
|
+
message,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
client.release();
|
|
150
|
+
await pool.end().catch(() => { });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OrganManifest } from './manifest';
|
|
2
|
+
export declare class OrganRegistry {
|
|
3
|
+
private manifests;
|
|
4
|
+
constructor(manifests?: OrganManifest[]);
|
|
5
|
+
register(manifest: OrganManifest): void;
|
|
6
|
+
get(organType: string): OrganManifest | undefined;
|
|
7
|
+
getAll(): OrganManifest[];
|
|
8
|
+
getAvailableOrganTypes(): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Discover installed or monorepo organ packages.
|
|
11
|
+
*/
|
|
12
|
+
static discover(searchRoots?: string[]): OrganRegistry;
|
|
13
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OrganRegistry = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const manifest_1 = require("./manifest");
|
|
10
|
+
const builtin_manifests_1 = require("./builtin-manifests");
|
|
11
|
+
class OrganRegistry {
|
|
12
|
+
manifests = new Map();
|
|
13
|
+
constructor(manifests) {
|
|
14
|
+
if (manifests) {
|
|
15
|
+
for (const m of manifests) {
|
|
16
|
+
this.manifests.set(m.organType, m);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
register(manifest) {
|
|
21
|
+
const validated = (0, manifest_1.validateOrganManifest)(manifest);
|
|
22
|
+
this.manifests.set(validated.organType, validated);
|
|
23
|
+
}
|
|
24
|
+
get(organType) {
|
|
25
|
+
return this.manifests.get(organType);
|
|
26
|
+
}
|
|
27
|
+
getAll() {
|
|
28
|
+
return Array.from(this.manifests.values());
|
|
29
|
+
}
|
|
30
|
+
getAvailableOrganTypes() {
|
|
31
|
+
return Array.from(this.manifests.keys());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discover installed or monorepo organ packages.
|
|
35
|
+
*/
|
|
36
|
+
static discover(searchRoots) {
|
|
37
|
+
const registry = new OrganRegistry();
|
|
38
|
+
const rootsToScan = searchRoots && searchRoots.length > 0
|
|
39
|
+
? searchRoots
|
|
40
|
+
: [
|
|
41
|
+
// 1. Monorepo organs folder relative to cli
|
|
42
|
+
node_path_1.default.resolve(__dirname, '../../packages/organs'),
|
|
43
|
+
// 2. Monorepo organs folder relative to cwd
|
|
44
|
+
node_path_1.default.resolve(process.cwd(), 'packages/organs'),
|
|
45
|
+
// 3. Node modules of current directory or global resolution
|
|
46
|
+
node_path_1.default.resolve(process.cwd(), 'node_modules/@siduri-x'),
|
|
47
|
+
node_path_1.default.resolve(__dirname, '../node_modules/@siduri-x'),
|
|
48
|
+
];
|
|
49
|
+
const visitedDirs = new Set();
|
|
50
|
+
for (const root of rootsToScan) {
|
|
51
|
+
if (!node_fs_1.default.existsSync(root) || visitedDirs.has(root))
|
|
52
|
+
continue;
|
|
53
|
+
visitedDirs.add(root);
|
|
54
|
+
try {
|
|
55
|
+
const entries = node_fs_1.default.readdirSync(root, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
const candidateDir = node_path_1.default.join(root, entry.name);
|
|
60
|
+
const manifestPath = node_path_1.default.join(candidateDir, 'organ-manifest.json');
|
|
61
|
+
if (node_fs_1.default.existsSync(manifestPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
const manifest = (0, manifest_1.validateOrganManifest)(raw, manifestPath);
|
|
65
|
+
registry.register(manifest);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.warn(`[Siduri CLI] Warning: failed to parse manifest at ${manifestPath}: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
// Continue scanning other roots
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// If running in a standalone environment without local package folders (e.g. via npx in an empty directory),
|
|
78
|
+
// register canonical built-in @siduri-x/* organ manifests as fallback.
|
|
79
|
+
if (registry.getAll().length === 0) {
|
|
80
|
+
for (const builtinManifest of builtin_manifests_1.BUILTIN_ORGAN_MANIFESTS) {
|
|
81
|
+
registry.register(builtinManifest);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return registry;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.OrganRegistry = OrganRegistry;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const discovery_1 = require("./discovery");
|
|
8
|
+
const generator_1 = require("./generator");
|
|
9
|
+
describe('Discovery & Dynamic Composition System Tests (Phase 3)', () => {
|
|
10
|
+
const rootOrgansDir = node_path_1.default.resolve(__dirname, '../../packages/organs');
|
|
11
|
+
test('Registry successfully discovers all 10 organ packages from workspace', () => {
|
|
12
|
+
const registry = discovery_1.OrganRegistry.discover([rootOrgansDir]);
|
|
13
|
+
const manifests = registry.getAll();
|
|
14
|
+
expect(manifests.length).toBe(10);
|
|
15
|
+
const organTypes = registry.getAvailableOrganTypes().sort();
|
|
16
|
+
expect(organTypes).toEqual([
|
|
17
|
+
'behavior',
|
|
18
|
+
'body',
|
|
19
|
+
'brain',
|
|
20
|
+
'ear',
|
|
21
|
+
'hands',
|
|
22
|
+
'knowledge',
|
|
23
|
+
'memory',
|
|
24
|
+
'observation',
|
|
25
|
+
'vision',
|
|
26
|
+
'voice',
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
test('Generates valid instances from discovered manifests', () => {
|
|
30
|
+
const registry = discovery_1.OrganRegistry.discover([rootOrgansDir]);
|
|
31
|
+
// Test Brain + Hands + Vision
|
|
32
|
+
const brain = registry.get('brain');
|
|
33
|
+
const hands = registry.get('hands');
|
|
34
|
+
const vision = registry.get('vision');
|
|
35
|
+
const files = (0, generator_1.generateInstanceFiles)({
|
|
36
|
+
name: 'robotics-agent',
|
|
37
|
+
selectedManifests: [brain, hands, vision],
|
|
38
|
+
});
|
|
39
|
+
const pkg = JSON.parse(files['package.json']);
|
|
40
|
+
expect(Object.keys(pkg.dependencies).sort()).toEqual([
|
|
41
|
+
'@siduri-x/brain',
|
|
42
|
+
'@siduri-x/core',
|
|
43
|
+
'@siduri-x/hands',
|
|
44
|
+
'@siduri-x/vision',
|
|
45
|
+
]);
|
|
46
|
+
const schema = JSON.parse(files['siduri.schema.json']);
|
|
47
|
+
expect(Object.keys(schema.properties.organs.properties).sort()).toEqual(['brain', 'hands', 'vision']);
|
|
48
|
+
const config = JSON.parse(files['siduri.config.json']);
|
|
49
|
+
expect(Object.keys(config.organs).sort()).toEqual(['brain', 'hands', 'vision']);
|
|
50
|
+
expect(files['.env.example']).toContain('OPENROUTER_API_KEY');
|
|
51
|
+
expect(files['.env.example']).toContain('ACTION_POLICY_SECRET');
|
|
52
|
+
expect(files['.env.example']).not.toContain('DATABASE_URL');
|
|
53
|
+
});
|
|
54
|
+
test('Validates manifest contract integrity during discovery', () => {
|
|
55
|
+
expect(() => {
|
|
56
|
+
const invalidRegistry = new discovery_1.OrganRegistry();
|
|
57
|
+
invalidRegistry.register({});
|
|
58
|
+
}).toThrow(/Invalid manifest/);
|
|
59
|
+
});
|
|
60
|
+
test('Fallback to builtin organ manifests when scanning non-existent search roots', () => {
|
|
61
|
+
const registry = discovery_1.OrganRegistry.discover(['/non-existent-directory/empty']);
|
|
62
|
+
const manifests = registry.getAll();
|
|
63
|
+
expect(manifests.length).toBe(10);
|
|
64
|
+
expect(registry.get('brain')).toBeDefined();
|
|
65
|
+
expect(registry.get('memory')).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|