Skip to content

Commit ebefb7b

Browse files
committed
fix(cli): isolate source development profile
1 parent c323c3d commit ebefb7b

22 files changed

Lines changed: 534 additions & 101 deletions

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ Architecture is documented in [ARCHITECTURE.md](./ARCHITECTURE.md).
9494
npm run dev # desktop app with HMR
9595
npm run dev:full # full build, then launch the desktop app
9696

97-
npm --workspace maka-agent exec -- maka # TUI
98-
npm --workspace maka-agent exec -- maka run "" # one non-interactive turn
97+
npm run cli:dev # TUI with the Maka Dev profile
98+
npm run cli:dev -- run "" # one non-interactive turn
9999
```
100100

101101
Evaluation commands and contracts live in [`packages/eval`](./packages/eval).

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,20 @@ npm run build
116116
Then start the TUI or run one Turn:
117117

118118
```sh
119-
npm --workspace maka-agent exec -- maka
120-
npm --workspace maka-agent exec -- maka run "Summarize this repository and identify its most important risk"
121-
npm --workspace maka-agent exec -- maka run --graph "Implement two independent slices, integrate them, then review the result"
122-
npm --workspace maka-agent exec -- maka --help
119+
npm run cli:dev
120+
npm run cli:dev -- run "Summarize this repository and identify its most important risk"
121+
npm run cli:dev -- run --graph "Implement two independent slices, integrate them, then review the result"
122+
npm run cli:dev -- --help
123123
```
124124

125125
The TUI also accepts `/graph on`, `/graph off`, and `/graph <task>`. Non-interactive
126126
`--graph` runs wait for the durable Graph to finish before printing the final
127127
supervisor output. Graph implementation operators use isolated Git worktrees, so
128128
the source project must be a clean Git worktree.
129129

130-
The CLI reads the same model connections and workspace configuration written by Desktop. Evaluation specs and adapters live in [`packages/eval`](./packages/eval).
130+
The repository CLI uses the same `Maka Dev` profile as a development Desktop build. The
131+
released `maka` binary continues to use the `Maka` profile; the two profiles are not copied or
132+
synchronized automatically. Evaluation specs and adapters live in [`packages/eval`](./packages/eval).
131133

132134
## Architecture
133135

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"test:dist:serial": "node scripts/run-workspace-tests-parallel.mjs --serial",
3333
"dev": "npm --workspace @maka/desktop run dev:hmr --",
3434
"dev:full": "npm run build && npm --workspace @maka/desktop run start",
35+
"cli:dev": "node packages/cli/dist/dev-cli.js",
3536
"build": "npm --workspace @maka/code-mode run build && npm --workspace @maka/core run build && npm --workspace @maka/storage run build && npm --workspace @maka/mcp run build && npm --workspace @maka/runtime run build && npm --workspace @maka/runtime-host run build && npm --workspace @maka/computer-use run build && npm --workspace @maka/eval run build && npm --workspace maka-agent run build && npm --workspace @maka/ui run build && npm --workspace @maka/desktop run build",
3637
"build:test": "npm run clean && npm --workspace @maka/code-mode run build && npm --workspace @maka/core run build && npm --workspace @maka/storage run build && npm --workspace @maka/mcp run build && npm --workspace @maka/runtime run build && npm --workspace @maka/runtime-host run build && npm --workspace @maka/computer-use run build && npm --workspace @maka/eval run build && npm --workspace maka-agent run build && npm --workspace @maka/ui run build && npm --workspace @maka/desktop run build:test",
3738
"clean": "node scripts/clean-build.mjs",

packages/cli/scripts/chmod-bin.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@
33
import { chmod } from 'node:fs/promises';
44
import { join } from 'node:path';
55

6-
await chmod(join(process.cwd(), 'dist', 'cli.js'), 0o755);
6+
await Promise.all(
7+
['cli.js', 'dev-cli.js'].map((entrypoint) =>
8+
chmod(join(process.cwd(), 'dist', entrypoint), 0o755),
9+
),
10+
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, test } from 'node:test';
3+
import { formatMakaResumeCommand, formatMakaResumeHint } from '../cli-invocation.js';
4+
5+
describe('Maka CLI invocation copy', () => {
6+
test('keeps release resume instructions on the release launcher', () => {
7+
assert.equal(
8+
formatMakaResumeHint('maka', 'session-1'),
9+
'Resume this session with:\n maka --resume session-1',
10+
);
11+
});
12+
13+
test('keeps development remote and cwd retries on the development launcher', () => {
14+
assert.equal(
15+
formatMakaResumeHint('npm run cli:dev --', 'session-2', { hostProfileId: 'office' }),
16+
'Resume this session with:\n npm run cli:dev -- --resume session-2 --host office',
17+
);
18+
assert.equal(
19+
formatMakaResumeCommand('npm run cli:dev --', 'session-2', { cwd: '<new-path>' }),
20+
'npm run cli:dev -- --resume session-2 --cwd <new-path>',
21+
);
22+
});
23+
});

packages/cli/src/__tests__/cli.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import assert from 'node:assert/strict';
22
import { spawn } from 'node:child_process';
33
import { once } from 'node:events';
4+
import { access, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
5+
import { tmpdir } from 'node:os';
6+
import { join } from 'node:path';
47
import { describe, test } from 'node:test';
5-
import { parseMakaCliArgs } from '../cli.js';
8+
import { fileURLToPath } from 'node:url';
9+
import { parseMakaCliArgs, runMakaCli } from '../cli.js';
610

711
describe('Maka CLI args', () => {
812
test('selects a Runtime Host and Project for TUI startup', () => {
@@ -13,6 +17,19 @@ describe('Maka CLI args', () => {
1317
});
1418
});
1519

20+
test('uses the active launcher in development help and rejects an empty profile name', async () => {
21+
const help = parseMakaCliArgs(['--help'], '0.1.0', 'npm run cli:dev --');
22+
assert.equal(help.kind, 'help');
23+
if (help.kind === 'help') {
24+
assert.match(help.text, /^Usage: npm run cli:dev --$/m);
25+
assert.doesNotMatch(help.text, /maka-agent/);
26+
}
27+
await assert.rejects(
28+
runMakaCli(['--version'], { dataProfileName: '', cliCommand: 'invalid' }),
29+
/profile name must be a non-empty path segment/,
30+
);
31+
});
32+
1633
test('establishes the fatal exit before reporting can throw', async () => {
1734
const cliUrl = new URL('../cli.js', import.meta.url).href;
1835
const childSource = `
@@ -29,4 +46,91 @@ describe('Maka CLI args', () => {
2946
assert.equal(signal, null);
3047
assert.equal(code, 1);
3148
});
49+
50+
test('compiled release and repository entries isolate profile writes', async (t) => {
51+
const root = await mkdtemp(join(tmpdir(), 'maka-cli-launch-profile-'));
52+
t.after(() => rm(root, { recursive: true, force: true }));
53+
const home = join(root, 'home');
54+
const applicationData = join(root, 'application-data');
55+
const releaseRoot = platformProfileRoot(home, applicationData, 'Maka');
56+
const developmentRoot = platformProfileRoot(home, applicationData, 'Maka Dev');
57+
const env = {
58+
...process.env,
59+
HOME: home,
60+
USERPROFILE: home,
61+
APPDATA: applicationData,
62+
XDG_CONFIG_HOME: applicationData,
63+
MAKA_TEST_RUNTIME_HOST_CREDENTIAL: 'opaque-token',
64+
};
65+
const profileArgs = [
66+
'runtime-host',
67+
'profile',
68+
'set',
69+
'--id',
70+
'office',
71+
'--name',
72+
'Office',
73+
'--tls-url',
74+
'wss://runtime.example.com/runtime-host',
75+
'--expected-root',
76+
'a'.repeat(64),
77+
'--credential-env',
78+
'MAKA_TEST_RUNTIME_HOST_CREDENTIAL',
79+
];
80+
81+
const development = await runCompiledCli('dev-cli.js', profileArgs, env);
82+
assert.equal(development.signal, null);
83+
assert.equal(development.code, 0, development.stderr);
84+
await assertProfileFiles(developmentRoot);
85+
await assert.rejects(access(releaseRoot), { code: 'ENOENT' });
86+
87+
await mkdir(releaseRoot, { recursive: true });
88+
await writeFile(join(releaseRoot, 'sentinel.txt'), 'release-data\n', 'utf8');
89+
assert.deepEqual(await readdir(releaseRoot), ['sentinel.txt']);
90+
assert.equal(await readFile(join(releaseRoot, 'sentinel.txt'), 'utf8'), 'release-data\n');
91+
92+
const developmentProfile = await readFile(
93+
join(developmentRoot, 'runtime-host-profiles.json'),
94+
'utf8',
95+
);
96+
const release = await runCompiledCli('cli.js', profileArgs, env);
97+
assert.equal(release.signal, null);
98+
assert.equal(release.code, 0, release.stderr);
99+
await assertProfileFiles(releaseRoot);
100+
assert.equal(
101+
await readFile(join(developmentRoot, 'runtime-host-profiles.json'), 'utf8'),
102+
developmentProfile,
103+
);
104+
});
32105
});
106+
107+
async function runCompiledCli(
108+
entrypoint: string,
109+
args: readonly string[],
110+
env: NodeJS.ProcessEnv,
111+
): Promise<{ code: number | null; signal: NodeJS.Signals | null; stderr: string }> {
112+
const child = spawn(
113+
process.execPath,
114+
[fileURLToPath(new URL(`../${entrypoint}`, import.meta.url)), ...args],
115+
{ env, stdio: ['ignore', 'ignore', 'pipe'], timeout: 15_000, killSignal: 'SIGKILL' },
116+
);
117+
let stderr = '';
118+
child.stderr.setEncoding('utf8');
119+
child.stderr.on('data', (chunk: string) => {
120+
stderr += chunk;
121+
});
122+
const [code, signal] = (await once(child, 'close')) as [number | null, NodeJS.Signals | null];
123+
return { code, signal, stderr };
124+
}
125+
126+
function platformProfileRoot(home: string, applicationData: string, profileName: string): string {
127+
if (process.platform === 'darwin') {
128+
return join(home, 'Library', 'Application Support', profileName);
129+
}
130+
return join(applicationData, profileName);
131+
}
132+
133+
async function assertProfileFiles(root: string): Promise<void> {
134+
await access(join(root, 'runtime-host-profiles.json'));
135+
await access(join(root, 'runtime-host-client', 'credentials.json'));
136+
}

packages/cli/src/__tests__/runtime-host-cli-context.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import assert from 'node:assert/strict';
2-
import { basename } from 'node:path';
2+
import { mkdtemp, rm } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import { basename, join } from 'node:path';
35
import { test } from 'node:test';
46
import { fileURLToPath } from 'node:url';
57
import {
68
connectRemoteRuntimeHostProfile,
9+
createClientRuntimeHostProfileCatalog,
710
type RuntimeHostConnection,
811
} from '@maka/runtime-host/client';
912
import {
@@ -165,6 +168,64 @@ test('remote CLI profiles pin root identity and resolve credential outside the p
165168
await context.close();
166169
});
167170

171+
test('remote CLI profile state and Client identity use the explicit Client Data Root', async (t) => {
172+
const clientDataRoot = await mkdtemp(join(tmpdir(), 'maka-cli-client-root-'));
173+
t.after(() => rm(clientDataRoot, { recursive: true, force: true }));
174+
const rootId = 'b'.repeat(64);
175+
await createClientRuntimeHostProfileCatalog(clientDataRoot).save(
176+
{
177+
id: 'office',
178+
name: 'Office',
179+
kind: 'remote',
180+
transport: { kind: 'tls', url: 'wss://runtime.example.com/runtime-host' },
181+
rootId,
182+
},
183+
'opaque-token',
184+
);
185+
let identityPath: string | undefined;
186+
let credential: string | undefined;
187+
const connection = {
188+
rootId,
189+
hostEpoch: 'host-remote',
190+
connectionId: 'connection-remote',
191+
selectedProtocol: 0,
192+
closed: new Promise<void>(() => {}),
193+
status: async () => ({ state: 'ready' }),
194+
subscribeConfigurationChanges: () => () => {},
195+
subscribeProjectCatalogChanges: () => () => {},
196+
subscribeSessionCatalogChanges: () => () => {},
197+
subscribeScheduledTaskChanges: () => () => {},
198+
close: async () => {},
199+
} as unknown as RuntimeHostConnection;
200+
201+
const context = await connectRuntimeHostCli(
202+
{
203+
rootPath: '/unused-local-root',
204+
clientDataRoot,
205+
surface: 'run',
206+
profileId: 'office',
207+
},
208+
{
209+
connectOrSpawn: async () => {
210+
throw new Error('remote profile must not use local discovery');
211+
},
212+
connectRemoteProfile: async (input) => {
213+
credential = input.credential;
214+
return connection;
215+
},
216+
loadClientInstanceId: async (path) => {
217+
identityPath = path;
218+
return '22222222-2222-4222-8222-222222222222';
219+
},
220+
readConnectionCatalog: async () => ({ revision: 1, defaultTarget: null, connections: [] }),
221+
},
222+
);
223+
224+
assert.equal(credential, 'opaque-token');
225+
assert.equal(identityPath, join(clientDataRoot, 'runtime-host-client.json'));
226+
await context.close();
227+
});
228+
168229
function hostRegistration(overrides: Partial<{ compatibilityEpoch: number }> = {}) {
169230
return {
170231
kind: 'maka-runtime-host' as const,

packages/cli/src/__tests__/runtime-host-run-command.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,18 @@ describe('Runtime Host maka run adapter', () => {
4343
return publicCommandContext(input);
4444
},
4545
},
46+
{ clientDataRoot: '/client-data', cliCommand: 'npm run cli:dev --' },
4647
);
4748

4849
assert.equal(exitCode, 2);
4950
assert.equal(contextCreations, 0);
5051
assert.match(stderr.join(''), /model_connection_disabled/);
51-
assert.match(stderr.join(''), /repair connection "openai-main" in `maka`/);
52+
assert.match(stderr.join(''), /repair connection "openai-main" in `npm run cli:dev --`/);
5253
});
5354

54-
test('routes a remote run through the selected Host profile and canonical Project', async () => {
55+
test('routes both launch roots through the selected Host profile and canonical Project', async () => {
56+
let selectedWorkspaceRoot: string | undefined;
57+
let selectedClientDataRoot: string | undefined;
5558
let selectedProfile: string | undefined;
5659
let contextInput: MakaRunContextInput | undefined;
5760
const connection = remoteReadinessConnection();
@@ -68,7 +71,9 @@ describe('Runtime Host maka run adapter', () => {
6871
newId: () => 'turn-remote',
6972
},
7073
{
71-
connect: async (_rootPath, profileId) => {
74+
connect: async (rootPath, profileId, clientDataRoot) => {
75+
selectedWorkspaceRoot = rootPath;
76+
selectedClientDataRoot = clientDataRoot;
7277
selectedProfile = profileId;
7378
return {
7479
connection,
@@ -88,9 +93,12 @@ describe('Runtime Host maka run adapter', () => {
8893
return publicCommandContext(input);
8994
},
9095
},
96+
{ clientDataRoot: '/client-data', cliCommand: 'npm run cli:dev --' },
9197
);
9298

9399
assert.equal(exitCode, 0);
100+
assert.equal(selectedWorkspaceRoot, '/runtime-host-data');
101+
assert.equal(selectedClientDataRoot, '/client-data');
94102
assert.equal(selectedProfile, 'office');
95103
assert.equal(contextInput?.projectId, 'project-1');
96104
});

packages/cli/src/cli-invocation.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export interface MakaResumeCommandOptions {
2+
readonly cwd?: string;
3+
readonly hostProfileId?: string;
4+
}
5+
6+
export function formatMakaResumeCommand(
7+
cliCommand: string,
8+
sessionId: string,
9+
options: MakaResumeCommandOptions = {},
10+
): string {
11+
return [
12+
`${cliCommand} --resume ${sessionId}`,
13+
...(options.cwd ? ['--cwd', options.cwd] : []),
14+
...(options.hostProfileId ? ['--host', options.hostProfileId] : []),
15+
].join(' ');
16+
}
17+
18+
export function formatMakaResumeHint(
19+
cliCommand: string,
20+
sessionId: string | null,
21+
options: MakaResumeCommandOptions = {},
22+
): string | null {
23+
if (!sessionId) return null;
24+
return `Resume this session with:\n ${formatMakaResumeCommand(cliCommand, sessionId, options)}`;
25+
}

0 commit comments

Comments
 (0)