Skip to content

Commit 6ae774b

Browse files
feat: Improved test command and added file syncing (#56)
* WIP: fixed the test command and added a promptSecret to collect the user's password initially * fix: Selectively sync either /usr/local/lib/codify or ~/.local/share/codify * Undo un-needed changes * feat: Added a block for non-mac os systems
1 parent 920fb9f commit 6ae774b

File tree

4 files changed

+68
-19
lines changed

4 files changed

+68
-19
lines changed

src/orchestrators/test.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11

22
import { OS, SpawnStatus } from 'codify-schemas';
33
import os from 'node:os';
4+
import fs from 'node:fs'
5+
import path from 'node:path';
46

57
import { PluginInitOrchestrator } from '../common/initialize-plugins.js';
6-
import { ProcessName, ctx, SubProcessName } from '../events/context.js';
8+
import { ProcessName, SubProcessName, ctx } from '../events/context.js';
79
import { Reporter } from '../ui/reporters/reporter.js';
810
import { StubReporter } from '../ui/reporters/stub-reporter.js';
11+
import { FileUtils } from '../utils/file.js';
912
import { sleep } from '../utils/index.js';
1013
import { spawn, spawnSafe } from '../utils/spawn.js';
1114
import { PlanOrchestrator, PlanOrchestratorResponse } from './plan.js';
1215
import { ValidateOrchestrator } from './validate.js';
16+
import { OsUtils } from '../utils/os-utils.js';
1317

1418
export interface TestArgs {
1519
path?: string;
@@ -20,13 +24,17 @@ export interface TestArgs {
2024

2125
export const TestOrchestrator = {
2226
async run(args: TestArgs, reporter: Reporter): Promise<void> {
27+
if (!OsUtils.isMacOS()) {
28+
throw new Error('Only a MacOS host is supported currently for testing');
29+
}
30+
2331
ctx.processStarted(ProcessName.TEST);
2432
reporter.silent = true;
2533

2634
ctx.subprocessStarted(SubProcessName.TEST_INITIALIZE_AND_VALIDATE);
2735
// Perform validation initially to ensure the project is valid
28-
const initializationResult = await PluginInitOrchestrator.run({ ...args, noProgress: true }, new StubReporter());
29-
await ValidateOrchestrator.run({ existing: initializationResult, noProgress: true }, new StubReporter());
36+
const initializationResult = await PluginInitOrchestrator.run({ ...args, noProgress: true }, reporter);
37+
await ValidateOrchestrator.run({ existing: initializationResult, noProgress: true }, reporter);
3038
ctx.subprocessFinished(SubProcessName.TEST_INITIALIZE_AND_VALIDATE);
3139

3240
await this.ensureVmIsInstalled(reporter, args.vmOs);
@@ -36,8 +44,14 @@ export const TestOrchestrator = {
3644
const vmName = this.generateVmName();
3745
await spawnSafe(`tart clone ${baseVmName} ${vmName}`, { interactive: true });
3846

47+
// We want to install the latest Codify version which usually exists in ~/.local/share/codify/client/current unless it's not there.
48+
const codifyInstall = (await FileUtils.dirExists('~/.local/share/codify/client/current'))
49+
? '~/.local/share/codify/client/current'
50+
: '/usr/local/lib/codify';
51+
3952
// Run this in the background. The user will have to manually exit the GUI to stop the test.
40-
spawnSafe(`tart run ${vmName}`, { interactive: true })
53+
// We bind mount the codify installation and the codify config directory. We choose not use :ro (read-only) because live changes are not supported in read-only mode.
54+
spawnSafe(`tart run ${vmName} --dir=codify-lib:${codifyInstall}:ro --dir=codify-config:${path.dirname(initializationResult.project.codifyFiles[0])}:ro`, { interactive: true })
4155
.finally(() => {
4256
ctx.subprocessFinished(SubProcessName.TEST_USER_CONTINUE_ON_VM);
4357
ctx.subprocessStarted(SubProcessName.TEST_DELETING_VM);
@@ -49,7 +63,7 @@ export const TestOrchestrator = {
4963
console.log('VM has been killed... exiting.')
5064
process.exit(1);
5165
})
52-
await sleep(10_000);
66+
await sleep(5000);
5367
await this.waitUntilVmIsReady(vmName);
5468

5569
ctx.subprocessFinished(SubProcessName.TEST_STARTING_VM);
@@ -58,12 +72,19 @@ export const TestOrchestrator = {
5872
// Install codify on the VM
5973
// await spawn(`tart exec ${vmName} /bin/bash -c "$(curl -fsSL https://releases.codifycli.com/install.sh)"`, { interactive: true });
6074
const { data: ip } = await spawnSafe(`tart ip ${vmName}`, { interactive: true });
61-
await spawn(`sshpass -p "admin" scp -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${initializationResult.project.codifyFiles[0]} admin@${ip}:~/codify.jsonc`, { interactive: true });
6275

63-
if (args.vmOs === OS.Darwin) {
64-
await spawn(`tart exec ${vmName} osascript -e "tell application \\"Terminal\\" to do script \\"cd ~/ && codify apply\\""`, { interactive: true });
65-
} else {
66-
await spawn(`tart exec ${vmName} gnome-terminal -- bash -c "cd ~/ && codify apply"`, { interactive: true });
76+
try {
77+
// Add symlinks to the bind mount locations.
78+
await spawn(`tart exec ${vmName} sudo ln -s /Volumes/My\\ Shared\\ Files/codify-lib/bin/codify /usr/local/bin/codify`, { interactive: true });
79+
await spawn(`tart exec ${vmName} ln -s /Volumes/My\\ Shared\\ Files/codify-config/${path.basename(initializationResult.project.codifyFiles[0])} /Users/admin/codify.jsonc`, { interactive: true });
80+
81+
// Launch terminal and run codify apply
82+
await (args.vmOs === OS.Darwin ? spawn(`tart exec ${vmName} osascript -e "tell application \\"Terminal\\" to do script \\"cd ~ && codify apply\\""`, { interactive: true }) : spawn(`tart exec ${vmName} gnome-terminal -- bash -c "cd ~/ && codify apply"`, { interactive: true }));
83+
84+
this.watchAndSyncFileChanges(initializationResult.project.codifyFiles[0], ip);
85+
86+
} catch (error) {
87+
ctx.log(`Error copying files to VM: ${error}`);
6788
}
6889

6990
ctx.subprocessFinished(SubProcessName.TEST_COPYING_OVER_CONFIGS_AND_OPENING_TERMINAL);
@@ -136,5 +157,28 @@ export const TestOrchestrator = {
136157

137158
await sleep(1000);
138159
}
160+
},
161+
162+
watchAndSyncFileChanges(filePath: string, ip: string): void {
163+
const watcher = fs.watch(filePath, { persistent: false }, async (eventType) => {
164+
if (eventType === 'change') {
165+
ctx.log('Config file changed, syncing to VM...');
166+
try {
167+
// Copy the updated config file to the VM
168+
// This command will fail but it causes the bind mount to update for some reason. (seems like a bug in Tart). Leave this here for now.
169+
await spawn(`sshpass -p "admin" scp -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${filePath} admin@${ip}:~/codify.jsonc`, { interactive: true });
170+
// ctx.log('Config file synced successfully');
171+
} catch (error) {
172+
// ctx.log(`Error syncing config file: ${error}`);
173+
}
174+
}
175+
});
176+
177+
// Clean up the watcher when the process finishes
178+
const cleanupWatcher = () => {
179+
watcher.close();
180+
};
181+
182+
process.once('exit', cleanupWatcher);
139183
}
140184
};

src/ui/reporters/default-reporter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const ProgressLabelMapping = {
3838
[SubProcessName.TEST_INITIALIZE_AND_VALIDATE]: 'Initializing and validating your configs',
3939
[SubProcessName.TEST_CHECKING_VM_INSTALLED]: 'Checking if VM is installed',
4040
[SubProcessName.TEST_STARTING_VM]: 'Starting VM',
41-
[SubProcessName.TEST_COPYING_OVER_CONFIGS_AND_OPENING_TERMINAL]: 'Copying over configs and opening terminal',
41+
[SubProcessName.TEST_COPYING_OVER_CONFIGS_AND_OPENING_TERMINAL]: 'Copying over configs and opening terminal (if a confirmation dialog appears within the VM, please confirm it.)',
4242
[SubProcessName.TEST_USER_CONTINUE_ON_VM]: 'Done setup! Please continue on the VM UI',
4343
[SubProcessName.TEST_DELETING_VM]: 'Deleting VM',
4444
}

src/utils/file.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ export class FileUtils {
2222
}
2323
}
2424

25+
static async dirExists(dirPath: string, throwIfExistsButNotFile = true): Promise<boolean> {
26+
try {
27+
const result = await fs.lstat(path.resolve(dirPath))
28+
if (throwIfExistsButNotFile && !result.isDirectory()) {
29+
throw new Error(`File found at ${dirPath} instead of a file`)
30+
}
31+
return true;
32+
} catch(e) {
33+
return false;
34+
}
35+
}
36+
2537
static async isDir(fileOrDir: string): Promise<boolean> {
2638
const lstat = await fs.lstat(path.resolve(fileOrDir))
2739
return lstat.isDirectory()

src/utils/spawn.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,7 @@ export async function spawnSafe(cmd: string, options?: SpawnOptions, pluginName?
6565
const initialCols = process.stdout.columns ?? 80;
6666
const initialRows = process.stdout.rows ?? 24;
6767

68-
// Mac OS uses -SN instead of -Sn
69-
let command;
70-
if (OsUtils.isMacOS()) {
71-
command = options?.requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -SN <<< "${password}" -E ${ShellUtils.getDefaultShell()} ${options?.interactive ? '-i' : ''} -c "${cmd.replaceAll('\'', '\\\'')}"` : cmd;
72-
} else {
73-
command = options?.requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -S <<< "${password}" -E ${ShellUtils.getDefaultShell()} ${options?.interactive ? '-i' : ''} -c '${cmd.replaceAll('\'', '\\\'')}'` : cmd;
74-
}
75-
68+
const command = options?.requiresRoot ? `sudo -k >/dev/null 2>&1; sudo -S <<< "${password}" -E ${ShellUtils.getDefaultShell()} ${options?.interactive ? '-i' : ''} -c "${cmd.replaceAll('"', '\\"')}"` : cmd;
7669
const args = options?.interactive ? ['-i', '-c', command] : ['-c', command]
7770

7871
// Run the command in a pty for interactivity

0 commit comments

Comments
 (0)