starmux 0.10.0 → 0.12.0
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/CONTRIBUTING.md +1 -1
- package/README.md +184 -59
- package/SECURITY.md +3 -3
- package/app/README.md +6 -2
- package/dist/src/access-control.js +23 -20
- package/dist/src/access-control.js.map +1 -1
- package/dist/src/access-dashboard.js +8 -2
- package/dist/src/access-dashboard.js.map +1 -1
- package/dist/src/cli/instances.js +87 -14
- package/dist/src/cli/instances.js.map +1 -1
- package/dist/src/cli/starmux-status.js +122 -0
- package/dist/src/cli/starmux-status.js.map +1 -0
- package/dist/src/cli/starmux.js +222 -49
- package/dist/src/cli/starmux.js.map +1 -1
- package/dist/src/deny-dashboard.js +2 -2
- package/dist/src/deny-dashboard.js.map +1 -1
- package/dist/src/instance-runtime.js +11 -2
- package/dist/src/instance-runtime.js.map +1 -1
- package/dist/src/instance-store.js +3 -1
- package/dist/src/instance-store.js.map +1 -1
- package/dist/src/ops-dashboard.js +4 -4
- package/dist/src/ops-dashboard.js.map +1 -1
- package/dist/src/permissions-dashboard.js +2 -2
- package/dist/src/permissions-dashboard.js.map +1 -1
- package/dist/src/starmux-migration.js +1 -1
- package/dist/src/starmux-migration.js.map +1 -1
- package/docs/ACCESS_CONTROL.md +5 -5
- package/docs/CLIENTS.md +11 -19
- package/docs/CONNECT_CHATGPT.md +4 -6
- package/docs/INSTALL.md +9 -5
- package/docs/INSTANCES.md +5 -2
- package/docs/OAUTH.md +6 -14
- package/docs/PLATFORMS.md +4 -4
- package/docs/README.md +2 -11
- package/docs/REMOTE_SETUP.md +13 -13
- package/docs/SECURITY.md +5 -5
- package/docs/assets/starmux-app-en.png +0 -0
- package/docs/assets/starmux-architecture.svg +315 -0
- package/package.json +1 -1
- package/docs/ARCHITECTURE.md +0 -51
- package/docs/MIGRATION.md +0 -37
- package/docs/RELEASE_STATUS.md +0 -22
- package/docs/TYPESCRIPT_MIGRATION.md +0 -56
- package/docs/VALIDATION_OAUTH.md +0 -79
- package/docs/VALIDATION_TUNNEL.md +0 -39
- package/docs/VALIDATION_WORKBUDDY.md +0 -59
- package/docs/assets/mcp-connection-route.svg +0 -104
- package/docs/assets/mcp-gateway-simple.svg +0 -62
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createAccessControl } from '../access-control.js';
|
|
2
|
+
import { exists, instancePaths } from '../instance-store.js';
|
|
3
|
+
import { shellQuote } from '../platform.js';
|
|
4
|
+
async function authentication(instance, status) {
|
|
5
|
+
const file = instancePaths(instance).access;
|
|
6
|
+
try {
|
|
7
|
+
const state = await (await createAccessControl(file, { initialize: false })).snapshot();
|
|
8
|
+
if (status.phase === 'running' && status.health === 'ok' && status.accessMode !== state.mode) {
|
|
9
|
+
return { mode: null, activeKeys: 0, oauth: null, problem: 'Authentication changed while checking. Run status again.' };
|
|
10
|
+
}
|
|
11
|
+
const incomplete = status.phase !== 'running' && instance.requiredAccess !== 'ingress' && state.mode !== instance.requiredAccess;
|
|
12
|
+
return { mode: state.mode, activeKeys: state.credentials.filter(c => c.status === 'active').length,
|
|
13
|
+
oauth: state.oauth ? { issuer: state.oauth.issuer, resource: state.oauth.resource, clientId: state.oauth.clientId, scope: state.oauth.scope, tokenAuth: state.oauth.tokenEndpointAuthMethod } : null,
|
|
14
|
+
problem: incomplete ? `This instance requires ${instance.requiredAccess} authentication before starting.` : null };
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
// A new, never-started instance has an ingress default, but a lost store must
|
|
18
|
+
// never be presented as an intentional switch to unauthenticated access.
|
|
19
|
+
if (error.code === 'ENOENT' && status.phase === 'stopped' && instance.requiredAccess === 'ingress' && !await exists(file + '.initialized')) {
|
|
20
|
+
return { mode: 'ingress', activeKeys: 0, oauth: null, problem: null };
|
|
21
|
+
}
|
|
22
|
+
return { mode: null, activeKeys: 0, oauth: null, problem: 'Authentication settings are unavailable. Inspect them with the Auth command below.' };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function formatStarmuxStatus(instance, status, config, options = {}) {
|
|
26
|
+
const auth = await authentication(instance, status);
|
|
27
|
+
// Omit `-i default` for the default instance to keep everyday single-instance output clean.
|
|
28
|
+
const omitSelector = options.isDefault === true && instance.name === 'default';
|
|
29
|
+
const selector = /^[A-Za-z0-9_.-]+$/.test(instance.name) && !instance.name.startsWith('-') ? instance.name : shellQuote(instance.name);
|
|
30
|
+
const flag = omitSelector ? '' : (instance.name.startsWith('-') ? ` --instance=${selector}` : ` -i ${selector}`);
|
|
31
|
+
const command = (action) => `starmux ${action}${flag}`;
|
|
32
|
+
const ready = status.phase === 'running' && status.health === 'ok';
|
|
33
|
+
const hasProblem = Boolean(status.configProblem || status.runtimeProblem || auth.problem);
|
|
34
|
+
const indicator = hasProblem ? '!' : ready ? '●' : status.phase === 'running' ? '◐' : '○';
|
|
35
|
+
const labels = {
|
|
36
|
+
ingress: 'None (no token required)',
|
|
37
|
+
'api-key': `API key (Bearer token; ${auth.activeKeys} active ${auth.activeKeys === 1 ? 'key' : 'keys'})`,
|
|
38
|
+
oauth: 'OAuth (browser sign-in)'
|
|
39
|
+
};
|
|
40
|
+
const saved = status.phase === 'running' ? ' (saved; live state unavailable)' : ' (saved for next start)';
|
|
41
|
+
const row = (label, value) => ' ' + label.padEnd(16) + value;
|
|
42
|
+
const codingCmd = command('up --code');
|
|
43
|
+
const mode = !config
|
|
44
|
+
? 'Unavailable'
|
|
45
|
+
: config.accessRole === 'trusted'
|
|
46
|
+
? (config.writeScope === 'home-only' ? 'Coding (Home sandbox · edits, shell & git)' : 'Coding (OS account · edits, shell & git)')
|
|
47
|
+
: config.mode === 'read-write'
|
|
48
|
+
? 'File writes (restricted)'
|
|
49
|
+
: `Read-only (run '${codingCmd}' to enable edits & shell)`;
|
|
50
|
+
const runtime = status.executionMode === 'background'
|
|
51
|
+
? 'Background · continues after this terminal closes'
|
|
52
|
+
: status.executionMode === 'foreground-owned'
|
|
53
|
+
? (status.runtime?.launcher === 'app' ? 'App-owned · stops when the app closes' : 'Foreground · Ctrl+C stops this instance')
|
|
54
|
+
: 'Stopped';
|
|
55
|
+
const title = omitSelector
|
|
56
|
+
? `${indicator} Starmux is ${status.phase}`
|
|
57
|
+
: `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
|
|
58
|
+
const lines = ['', title];
|
|
59
|
+
if (ready && status.port) {
|
|
60
|
+
lines.push(row('Listen', `http://127.0.0.1:${status.port}/mcp (this computer only)`));
|
|
61
|
+
}
|
|
62
|
+
lines.push(row('Workspace', config?.projectRoot ?? 'Unavailable'), row('Mode', mode), row('Auth', auth.mode ? labels[auth.mode] + (ready ? '' : saved) : 'Unavailable'), row('Runtime', runtime));
|
|
63
|
+
if (ready && status.port && auth.mode === 'oauth' && auth.oauth) {
|
|
64
|
+
lines.push(row('Client URL', auth.oauth.resource), row('Issuer', auth.oauth.issuer));
|
|
65
|
+
}
|
|
66
|
+
if (status.configProblem || status.runtimeProblem)
|
|
67
|
+
lines.push(row('Problem', status.configProblem ?? status.runtimeProblem));
|
|
68
|
+
if (auth.problem)
|
|
69
|
+
lines.push(row('Auth issue', auth.problem));
|
|
70
|
+
const keyCommand = command('auth key "My AI Client"');
|
|
71
|
+
if (ready && !auth.problem) {
|
|
72
|
+
lines.push('', 'Connect an AI client (MCP settings)');
|
|
73
|
+
lines.push(row('Transport', 'Streamable HTTP'));
|
|
74
|
+
if (auth.mode === 'ingress') {
|
|
75
|
+
lines.push(row('URL (Local)', `http://127.0.0.1:${status.port}/mcp (AI client on this computer)`), row('URL (Remote)', `https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`), row('Authentication', `None (optional Bearer API key: ${keyCommand})`));
|
|
76
|
+
}
|
|
77
|
+
else if (auth.mode === 'api-key') {
|
|
78
|
+
lines.push(row('URL (Local)', `http://127.0.0.1:${status.port}/mcp (AI client on this computer)`), row('URL (Remote)', `https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`), row('Authentication', `Bearer API key (${auth.activeKeys} active ${auth.activeKeys === 1 ? 'key' : 'keys'})`), row('Header', 'Authorization: Bearer <YOUR_API_KEY>'));
|
|
79
|
+
if (!auth.activeKeys) {
|
|
80
|
+
lines.push(row('Create key', `${keyCommand} (no active keys; required before connecting)`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (auth.mode === 'oauth' && auth.oauth) {
|
|
84
|
+
lines.push(row('URL (Remote)', auth.oauth.resource), row('Authentication', 'OAuth (browser sign-in)'), row('Issuer', auth.oauth.issuer), row('Client ID', `${auth.oauth.clientId} (Scope: ${auth.oauth.scope} · Token auth: ${auth.oauth.tokenAuth})`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
lines.push('', 'Commands');
|
|
88
|
+
if (auth.problem) {
|
|
89
|
+
lines.push(row('Check auth', command('auth')));
|
|
90
|
+
}
|
|
91
|
+
else if (!ready) {
|
|
92
|
+
if (status.phase === 'stopped' && !status.configProblem && !status.runtimeProblem) {
|
|
93
|
+
if (auth.mode === 'api-key' && !auth.activeKeys)
|
|
94
|
+
lines.push(row('Create key', keyCommand));
|
|
95
|
+
lines.push(row('Start', command('up')), row('Start coding', codingCmd));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
lines.push(row('Diagnose', command('doctor')));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (auth.mode === 'api-key' && auth.activeKeys) {
|
|
103
|
+
lines.push(row('Verify', `STARMUX_TOKEN=<key> ${command('verify')}`), row('New API key', keyCommand));
|
|
104
|
+
}
|
|
105
|
+
else if (auth.mode === 'oauth') {
|
|
106
|
+
lines.push(row('Check OAuth', command('auth oauth doctor')), row('Verify', command('verify')));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push(row('Verify', command('verify')));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (status.configState === 'pending')
|
|
113
|
+
lines.push(row('Apply config', command('config apply')));
|
|
114
|
+
lines.push(row('Auth & keys', command('auth')));
|
|
115
|
+
if (ready)
|
|
116
|
+
lines.push(row('Dashboard', `${command('dashboard')} (view & copy settings in browser)`));
|
|
117
|
+
lines.push(row('Logs', command('logs -f')));
|
|
118
|
+
if (status.phase === 'running')
|
|
119
|
+
lines.push(row('Stop', command('down')));
|
|
120
|
+
return lines.join('\n') + '\n';
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=starmux-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starmux-status.js","sourceRoot":"","sources":["../../../src/cli/starmux-status.ts"],"names":[],"mappings":"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","sourcesContent":["import {createAccessControl} from '../access-control.js';\nimport type {instanceStatus} from '../instance-runtime.js';\nimport {exists,instancePaths,type Instance,type InstanceConfig} from '../instance-store.js';\nimport {shellQuote} from '../platform.js';\n\ntype Status=Awaited<ReturnType<typeof instanceStatus>>;\ntype AuthMode='ingress'|'api-key'|'oauth';\ntype OAuthSummary={issuer:string;resource:string;clientId:string;scope:string;tokenAuth:string};\ntype AuthSummary={mode:AuthMode|null;activeKeys:number;oauth:OAuthSummary|null;problem:string|null};\n\nasync function authentication(instance:Instance,status:Status):Promise<AuthSummary>{\n const file=instancePaths(instance).access;\n try{\n const state=await (await createAccessControl(file,{initialize:false})).snapshot();\n if(status.phase==='running'&&status.health==='ok'&&status.accessMode!==state.mode){\n return {mode:null,activeKeys:0,oauth:null,problem:'Authentication changed while checking. Run status again.'};\n }\n const incomplete=status.phase!=='running'&&instance.requiredAccess!=='ingress'&&state.mode!==instance.requiredAccess;\n return {mode:state.mode,activeKeys:state.credentials.filter(c=>c.status==='active').length,\n oauth:state.oauth?{issuer:state.oauth.issuer,resource:state.oauth.resource,clientId:state.oauth.clientId,scope:state.oauth.scope,tokenAuth:state.oauth.tokenEndpointAuthMethod}:null,\n problem:incomplete?`This instance requires ${instance.requiredAccess} authentication before starting.`:null};\n }catch(error){\n // A new, never-started instance has an ingress default, but a lost store must\n // never be presented as an intentional switch to unauthenticated access.\n if((error as NodeJS.ErrnoException).code==='ENOENT'&&status.phase==='stopped'&&instance.requiredAccess==='ingress'&&!await exists(file+'.initialized')){\n return {mode:'ingress',activeKeys:0,oauth:null,problem:null};\n }\n return {mode:null,activeKeys:0,oauth:null,problem:'Authentication settings are unavailable. Inspect them with the Auth command below.'};\n }\n}\n\nexport async function formatStarmuxStatus(instance:Instance,status:Status,config:InstanceConfig|undefined,options:{isDefault?:boolean}={}){\n const auth=await authentication(instance,status);\n // Omit `-i default` for the default instance to keep everyday single-instance output clean.\n const omitSelector=options.isDefault===true&&instance.name==='default';\n const selector=/^[A-Za-z0-9_.-]+$/.test(instance.name)&&!instance.name.startsWith('-')?instance.name:shellQuote(instance.name);\n const flag=omitSelector?'':(instance.name.startsWith('-')?` --instance=${selector}`:` -i ${selector}`);\n const command=(action:string)=>`starmux ${action}${flag}`;\n const ready=status.phase==='running'&&status.health==='ok';\n const hasProblem=Boolean(status.configProblem||status.runtimeProblem||auth.problem);\n const indicator=hasProblem?'!':ready?'●':status.phase==='running'?'◐':'○';\n const labels={\n ingress:'None (no token required)',\n 'api-key':`API key (Bearer token; ${auth.activeKeys} active ${auth.activeKeys===1?'key':'keys'})`,\n oauth:'OAuth (browser sign-in)'\n };\n const saved=status.phase==='running'?' (saved; live state unavailable)':' (saved for next start)';\n const row=(label:string,value:string)=>' '+label.padEnd(16)+value;\n const codingCmd=command('up --code');\n const mode=!config\n ?'Unavailable'\n :config.accessRole==='trusted'\n ?(config.writeScope==='home-only'?'Coding (Home sandbox · edits, shell & git)':'Coding (OS account · edits, shell & git)')\n :config.mode==='read-write'\n ?'File writes (restricted)'\n :`Read-only (run '${codingCmd}' to enable edits & shell)`;\n const runtime=status.executionMode==='background'\n ?'Background · continues after this terminal closes'\n :status.executionMode==='foreground-owned'\n ?(status.runtime?.launcher==='app'?'App-owned · stops when the app closes':'Foreground · Ctrl+C stops this instance')\n :'Stopped';\n const title=omitSelector\n ?`${indicator} Starmux is ${status.phase}`\n :`${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;\n const lines=['',title];\n if(ready&&status.port){\n lines.push(row('Listen',`http://127.0.0.1:${status.port}/mcp (this computer only)`));\n }\n lines.push(\n row('Workspace',config?.projectRoot??'Unavailable'),\n row('Mode',mode),\n row('Auth',auth.mode?labels[auth.mode]+(ready?'':saved):'Unavailable'),\n row('Runtime',runtime)\n );\n if(ready&&status.port&&auth.mode==='oauth'&&auth.oauth){\n lines.push(row('Client URL',auth.oauth.resource),row('Issuer',auth.oauth.issuer));\n }\n if(status.configProblem||status.runtimeProblem)lines.push(row('Problem',status.configProblem??status.runtimeProblem!));\n if(auth.problem)lines.push(row('Auth issue',auth.problem));\n\n const keyCommand=command('auth key \"My AI Client\"');\n if(ready&&!auth.problem){\n lines.push('','Connect an AI client (MCP settings)');\n lines.push(row('Transport','Streamable HTTP'));\n if(auth.mode==='ingress'){\n lines.push(\n row('URL (Local)',`http://127.0.0.1:${status.port}/mcp (AI client on this computer)`),\n row('URL (Remote)',`https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`),\n row('Authentication',`None (optional Bearer API key: ${keyCommand})`)\n );\n }else if(auth.mode==='api-key'){\n lines.push(\n row('URL (Local)',`http://127.0.0.1:${status.port}/mcp (AI client on this computer)`),\n row('URL (Remote)',`https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:${status.port})`),\n row('Authentication',`Bearer API key (${auth.activeKeys} active ${auth.activeKeys===1?'key':'keys'})`),\n row('Header','Authorization: Bearer <YOUR_API_KEY>')\n );\n if(!auth.activeKeys){\n lines.push(row('Create key',`${keyCommand} (no active keys; required before connecting)`));\n }\n }else if(auth.mode==='oauth'&&auth.oauth){\n lines.push(\n row('URL (Remote)',auth.oauth.resource),\n row('Authentication','OAuth (browser sign-in)'),\n row('Issuer',auth.oauth.issuer),\n row('Client ID',`${auth.oauth.clientId} (Scope: ${auth.oauth.scope} · Token auth: ${auth.oauth.tokenAuth})`)\n );\n }\n }\n\n lines.push('','Commands');\n if(auth.problem){\n lines.push(row('Check auth',command('auth')));\n }else if(!ready){\n if(status.phase==='stopped'&&!status.configProblem&&!status.runtimeProblem){\n if(auth.mode==='api-key'&&!auth.activeKeys)lines.push(row('Create key',keyCommand));\n lines.push(row('Start',command('up')),row('Start coding',codingCmd));\n }else{\n lines.push(row('Diagnose',command('doctor')));\n }\n }else{\n if(auth.mode==='api-key'&&auth.activeKeys){\n lines.push(\n row('Verify',`STARMUX_TOKEN=<key> ${command('verify')}`),\n row('New API key',keyCommand)\n );\n }else if(auth.mode==='oauth'){\n lines.push(\n row('Check OAuth',command('auth oauth doctor')),\n row('Verify',command('verify'))\n );\n }else{\n lines.push(row('Verify',command('verify')));\n }\n }\n if(status.configState==='pending')lines.push(row('Apply config',command('config apply')));\n lines.push(row('Auth & keys',command('auth')));\n if(ready)lines.push(row('Dashboard',`${command('dashboard')} (view & copy settings in browser)`));\n lines.push(row('Logs',command('logs -f')));\n if(status.phase==='running')lines.push(row('Stop',command('down')));\n return lines.join('\\n')+'\\n';\n}\n"]}
|
package/dist/src/cli/starmux.js
CHANGED
|
@@ -9,10 +9,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
9
9
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
10
10
|
import { createAccessControl } from '../access-control.js';
|
|
11
11
|
import { instanceStatus, instanceRequest, runInstance } from '../instance-runtime.js';
|
|
12
|
-
import { atomicPrivate, availablePort, changeRegistry, createInstance, ensureAccessReady, exists, instanceConfigSchema, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig } from '../instance-store.js';
|
|
12
|
+
import { atomicPrivate, availablePort, changeRegistry, configJSONSchema, createInstance, ensureAccessReady, exists, instanceConfigSchema, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig } from '../instance-store.js';
|
|
13
13
|
import { handle } from './instances.js';
|
|
14
|
-
import { desktopCommanderEntry
|
|
15
|
-
|
|
14
|
+
import { desktopCommanderEntry } from '../runtime-paths.js';
|
|
15
|
+
import { shellQuote } from '../platform.js';
|
|
16
|
+
import { formatStarmuxStatus } from './starmux-status.js';
|
|
17
|
+
const version = '0.12.0';
|
|
16
18
|
const strings = ['instance', 'port', 'lines', 'expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'revision', 'patch', 'file', 'output', 'name', 'target', 'mode', 'app-parent-pid'];
|
|
17
19
|
const booleans = ['help', 'version', 'json', 'write', 'code', 'read', 'yes', 'foreground', 'follow', 'archived', 'all', 'effective', 'link', 'allow-loopback-http', 'token-stdin', 'no-open', 'token'];
|
|
18
20
|
const options = {};
|
|
@@ -39,22 +41,50 @@ function fail(message) { throw Error(message); }
|
|
|
39
41
|
const absolute = (value) => path.resolve(value === '~' ? homedir() : value.startsWith('~/') ? path.join(homedir(), value.slice(2)) : value);
|
|
40
42
|
const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
41
43
|
function help() {
|
|
42
|
-
const
|
|
44
|
+
const requested = command === 'help' ? positional[0] : command;
|
|
45
|
+
const topic = requested === 'stop' ? 'down' : requested;
|
|
43
46
|
const sections = {
|
|
44
47
|
up: 'up [path] [--read | --write | --code] [-y] [--port N] [--foreground]\n Initialize and start. First use defaults to CWD; later starts reuse saved settings.',
|
|
45
48
|
init: 'init [path] [--read | --write | --code] [-y] [--port N]\n Save settings without starting. --write and --code enable full account coding permissions.',
|
|
46
|
-
auth: 'auth\n auth key "Client name" [--expires 30d]\n auth mode ingress|api-key|oauth\n auth revoke ID\n auth oauth init-keycloak --resource URL --callback URL\n auth oauth setup --issuer URL --resource URL --client-id ID --subject USER_ID\n auth oauth doctor',
|
|
47
|
-
config: 'config show|
|
|
49
|
+
auth: 'auth\n auth key "Client name" [--expires 30d]\n auth mode none|ingress|api-key|oauth\n auth revoke ID\n auth oauth init-keycloak --resource URL --callback URL\n auth oauth setup --issuer URL --resource URL --client-id ID --subject USER_ID\n auth oauth doctor',
|
|
50
|
+
config: 'config [show] [--effective]\n config edit | diff | apply | validate | path | schema | recover\n config default NAME | rename NEW_NAME -i SOURCE | copy NEW_NAME -i SOURCE\n config archive -i NAME | restore -i NAME | import FILE [-i NAME] [--link]\n config export [--output FILE] | secret set|remove NAME',
|
|
48
51
|
dashboard: 'dashboard [--no-open | --token]\n Open the local Access console. --no-open prints a private login link.',
|
|
49
|
-
verify: 'verify [--token-stdin]\n Test MCP initialization and
|
|
52
|
+
verify: 'verify [--token-stdin]\n Test MCP initialization, tool discovery and workspace read. Authenticated instances use STARMUX_TOKEN or stdin.',
|
|
50
53
|
doctor: 'doctor\n Check Node, bundled dependencies, workspace, configuration, port and authentication.',
|
|
51
54
|
logs: 'logs [-f | --follow] [--lines 1..200]\n Follow continues across restarts. Ctrl+C exits the follower only.',
|
|
52
55
|
migrate: 'migrate [registry|setup|live] [-y]\n No source: preview. Select a source to migrate, retaining originals.',
|
|
53
|
-
status: 'status [--all]\n Show the selected instance, its actual endpoint and
|
|
54
|
-
ls: 'ls [--archived]\n List local instances.',
|
|
55
|
-
|
|
56
|
+
status: 'status [--all]\n Show the selected instance, its actual endpoint and connection guide.',
|
|
57
|
+
ls: 'ls [--archived]\n List local workspace instances.',
|
|
58
|
+
rm: 'rm [NAME] (alias for: config archive)\n Stop and archive an instance.',
|
|
59
|
+
down: 'down (alias: stop)\n Stop the selected instance.',
|
|
60
|
+
permissions: 'permissions [status | token]\n permissions disable-tool|enable-tool|deny-directory|allow-directory TARGET\n Manage the selected instance’s tool and directory permissions.',
|
|
61
|
+
privacy: 'privacy [--all] [--target PATH]\n Check privacy access through the running instance.'
|
|
56
62
|
};
|
|
57
|
-
|
|
63
|
+
if (topic && topic !== 'help' && !sections[topic])
|
|
64
|
+
fail('Unknown command: ' + topic + '. Run starmux --help.');
|
|
65
|
+
const overview = [
|
|
66
|
+
'Connect AI clients to your computer’s files, shell and development tools.',
|
|
67
|
+
'',
|
|
68
|
+
'Workspace Lifecycle:',
|
|
69
|
+
' starmux up [path] [--code] Start in the background (default: Read-only)',
|
|
70
|
+
' starmux down Stop the running instance (alias: stop)',
|
|
71
|
+
' starmux status Show health, endpoint and connection guide',
|
|
72
|
+
' starmux logs [-f] View or stream gateway logs',
|
|
73
|
+
'',
|
|
74
|
+
'Access & Verification:',
|
|
75
|
+
' starmux auth Manage auth mode, API keys and OAuth',
|
|
76
|
+
' starmux dashboard Open the local Web Access Console',
|
|
77
|
+
' starmux verify Test local MCP handshake and workspace read',
|
|
78
|
+
' starmux doctor Check runtime, config, port and auth state',
|
|
79
|
+
'',
|
|
80
|
+
'Multi-Instance & Config:',
|
|
81
|
+
' starmux ls List workspace instances',
|
|
82
|
+
' starmux rm [name] Stop and archive an instance',
|
|
83
|
+
' starmux init [path] Save instance settings without starting',
|
|
84
|
+
' starmux config Inspect, edit or apply configuration',
|
|
85
|
+
' starmux migrate Import settings from a legacy installation'
|
|
86
|
+
].join('\n');
|
|
87
|
+
process.stdout.write(`Starmux ${version}\n\n${sections[topic ?? ''] ?? overview}\n\nGlobal: -i, --instance NAME_OR_ID --json -h, --help\nUse starmux COMMAND --help for details.\n`);
|
|
58
88
|
}
|
|
59
89
|
async function confirm(message) {
|
|
60
90
|
if (yes())
|
|
@@ -118,6 +148,16 @@ async function configure() {
|
|
|
118
148
|
await rememberConsent(instance, config);
|
|
119
149
|
return instance;
|
|
120
150
|
}
|
|
151
|
+
async function isDefaultInstance(instance) {
|
|
152
|
+
const registry = await readRegistry().catch(() => ({ defaultInstanceId: null, instances: [] }));
|
|
153
|
+
return instance.name === 'default' && (registry.defaultInstanceId === instance.id || registry.instances.filter(i => !i.archived).length <= 1);
|
|
154
|
+
}
|
|
155
|
+
async function instanceFlag(instance) {
|
|
156
|
+
if (await isDefaultInstance(instance))
|
|
157
|
+
return '';
|
|
158
|
+
const selector = /^[A-Za-z0-9_.-]+$/.test(instance.name) && !instance.name.startsWith('-') ? instance.name : shellQuote(instance.name);
|
|
159
|
+
return instance.name.startsWith('-') ? `--instance=${selector}` : `-i ${selector}`;
|
|
160
|
+
}
|
|
121
161
|
async function show(instance) {
|
|
122
162
|
const status = await instanceStatus(instance);
|
|
123
163
|
if (json) {
|
|
@@ -125,12 +165,7 @@ async function show(instance) {
|
|
|
125
165
|
return;
|
|
126
166
|
}
|
|
127
167
|
const config = status.phase === 'running' ? status.active?.config : (await loadInstanceConfig(instance).catch(() => null))?.config;
|
|
128
|
-
process.stdout.write(
|
|
129
|
-
if (status.port && status.phase === 'running')
|
|
130
|
-
process.stdout.write(`MCP http://127.0.0.1:${status.port}/mcp\nDashboard http://127.0.0.1:${status.port}/access\n`);
|
|
131
|
-
if (status.configProblem || status.runtimeProblem)
|
|
132
|
-
process.stdout.write(`Problem ${status.configProblem ?? status.runtimeProblem}\n`);
|
|
133
|
-
process.stdout.write(`Logs starmux logs -f -i ${JSON.stringify(instance.name)}\n`);
|
|
168
|
+
process.stdout.write(await formatStarmuxStatus(instance, status, config, { isDefault: await isDefaultInstance(instance) }));
|
|
134
169
|
}
|
|
135
170
|
async function start(instance) {
|
|
136
171
|
const state = await instanceStatus(instance);
|
|
@@ -145,7 +180,7 @@ async function start(instance) {
|
|
|
145
180
|
if (!await availablePort(config.port))
|
|
146
181
|
fail(`Port ${config.port} is in use. Choose another with starmux up --port PORT.`);
|
|
147
182
|
if (flags.foreground || string('app-parent-pid')) {
|
|
148
|
-
await runInstance(instance, { json, ...(string('app-parent-pid') ? { appParentPid: Number(string('app-parent-pid')) } : {}) });
|
|
183
|
+
await runInstance(instance, { json, ...(!json ? { onReady: () => show(instance) } : {}), ...(string('app-parent-pid') ? { appParentPid: Number(string('app-parent-pid')) } : {}) });
|
|
149
184
|
return;
|
|
150
185
|
}
|
|
151
186
|
const log = path.join(instancePaths(instance).logs, 'supervisor.log');
|
|
@@ -189,10 +224,13 @@ async function verify(instance) {
|
|
|
189
224
|
fail('Start this instance with starmux up first.');
|
|
190
225
|
const token = await bearer();
|
|
191
226
|
const client = new Client({ name: 'starmux-verify', version });
|
|
192
|
-
const
|
|
227
|
+
const endpoint = `http://127.0.0.1:${status.port}/mcp`;
|
|
228
|
+
const transport = new StreamableHTTPClientTransport(new URL(endpoint), { requestInit: { headers: token ? { Authorization: 'Bearer ' + token } : {} } });
|
|
229
|
+
let toolCount = 0;
|
|
193
230
|
try {
|
|
194
231
|
await client.connect(transport);
|
|
195
|
-
await client.listTools();
|
|
232
|
+
const listed = await client.listTools();
|
|
233
|
+
toolCount = listed.tools.length;
|
|
196
234
|
const result = await client.callTool({ name: 'list_directory', arguments: { path: '.', depth: 0 } });
|
|
197
235
|
if (result.isError)
|
|
198
236
|
fail('Directory read failed.');
|
|
@@ -205,8 +243,15 @@ async function verify(instance) {
|
|
|
205
243
|
}
|
|
206
244
|
if (json)
|
|
207
245
|
print({ result: 'PASS', instanceId: instance.id, port: status.port, remoteClientVerified: false });
|
|
208
|
-
else
|
|
209
|
-
|
|
246
|
+
else {
|
|
247
|
+
const cfg = status.active?.config;
|
|
248
|
+
const mode = cfg?.accessRole === 'trusted' ? 'Coding mode' : 'Read-only mode';
|
|
249
|
+
const root = cfg?.projectRoot ?? '.';
|
|
250
|
+
process.stdout.write(`✓ MCP handshake ${endpoint}\n` +
|
|
251
|
+
`✓ Tool discovery ${toolCount} tools available (${mode})\n` +
|
|
252
|
+
`✓ Workspace read ${root}\n` +
|
|
253
|
+
`PASS · MCP handshake, tool discovery and workspace read.\n`);
|
|
254
|
+
}
|
|
210
255
|
}
|
|
211
256
|
function expires(value) {
|
|
212
257
|
if (!value)
|
|
@@ -222,36 +267,85 @@ async function auth(instance) {
|
|
|
222
267
|
const op = positional[0];
|
|
223
268
|
if (!op) {
|
|
224
269
|
const state = await access.snapshot();
|
|
225
|
-
if (json)
|
|
270
|
+
if (json) {
|
|
226
271
|
print(state);
|
|
227
|
-
|
|
228
|
-
process.stdout.write(`Authentication: ${state.mode}\n`);
|
|
229
|
-
for (const c of state.credentials)
|
|
230
|
-
process.stdout.write(`${c.id} ${c.name} ${c.status}\n`);
|
|
231
|
-
for (const c of state.oauthConnections)
|
|
232
|
-
process.stdout.write(`${c.id} OAuth ${c.clientId} ${c.revokedAt ? 'revoked' : 'active'}\n`);
|
|
272
|
+
return;
|
|
233
273
|
}
|
|
274
|
+
const flag = await instanceFlag(instance);
|
|
275
|
+
const cmd = (action) => `starmux ${action}${flag ? ' ' + flag : ''}`;
|
|
276
|
+
const activeKeys = state.credentials.filter(c => c.status === 'active').length;
|
|
277
|
+
const modeDesc = state.mode === 'ingress'
|
|
278
|
+
? 'None (no Starmux token · local or ingress proxy)'
|
|
279
|
+
: state.mode === 'api-key'
|
|
280
|
+
? `API key (Bearer token · ${activeKeys} active)`
|
|
281
|
+
: 'OAuth (browser sign-in)';
|
|
282
|
+
const lines = [
|
|
283
|
+
...(flag ? [`Instance ${instance.name}`] : []),
|
|
284
|
+
`Auth mode ${modeDesc}`,
|
|
285
|
+
`API keys ${activeKeys} active (${state.credentials.length} total)`,
|
|
286
|
+
`OAuth ${state.oauth ? `${state.oauth.issuer} (${state.oauthConnections.filter(c => !c.revokedAt).length} active sessions)` : 'Not configured'}`
|
|
287
|
+
];
|
|
288
|
+
if (state.credentials.length) {
|
|
289
|
+
lines.push('', 'API Keys');
|
|
290
|
+
for (const c of state.credentials) {
|
|
291
|
+
const exp = c.expiresAt ? ` expires ${c.expiresAt.slice(0, 10)}` : '';
|
|
292
|
+
lines.push(` ${c.id.padEnd(26)} ${c.name.padEnd(20)} ${c.status}${exp}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (state.oauthConnections.length) {
|
|
296
|
+
lines.push('', 'OAuth Sessions');
|
|
297
|
+
for (const c of state.oauthConnections) {
|
|
298
|
+
lines.push(` ${c.id.padEnd(26)} OAuth ${c.clientId.padEnd(14)} ${c.revokedAt ? 'revoked' : 'active'}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
lines.push('', 'Commands', ` Create API key ${cmd('auth key "My AI Client"')}`, ` Switch mode ${cmd('auth mode <none|api-key|oauth>')}`, ...(activeKeys > 0 || state.oauthConnections.some(c => !c.revokedAt) ? [` Revoke access ${cmd('auth revoke <ID>')}`] : []));
|
|
302
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
234
303
|
return;
|
|
235
304
|
}
|
|
236
305
|
if (op === 'key') {
|
|
237
306
|
const result = await access.create(positional[1] ?? '', expires(string('expires')), true);
|
|
238
307
|
await changeRegistry(async (r) => { r.instances.find(i => i.id === instance.id).requiredAccess = 'api-key'; });
|
|
239
|
-
if (json)
|
|
308
|
+
if (json) {
|
|
240
309
|
print({ ...result, mode: 'api-key' });
|
|
241
|
-
|
|
242
|
-
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const cfg = (await loadInstanceConfig(instance).catch(() => null))?.config;
|
|
313
|
+
const port = cfg?.port ?? 18893;
|
|
314
|
+
process.stdout.write(`API key enabled for ${result.name} (ID: ${result.id})\n` +
|
|
315
|
+
`Copy this key now; it is shown only once:\n\n` +
|
|
316
|
+
` ${result.key}\n\n` +
|
|
317
|
+
`Option A — Fill in AI client form UI:\n` +
|
|
318
|
+
` Transport Streamable HTTP\n` +
|
|
319
|
+
` URL (Local) http://127.0.0.1:${port}/mcp\n` +
|
|
320
|
+
` URL (Remote) https://<your-domain-or-tunnel>/mcp\n` +
|
|
321
|
+
` Authentication Bearer API key\n` +
|
|
322
|
+
` Header Authorization: Bearer ${result.key}\n\n` +
|
|
323
|
+
`Option B — Or paste JSON config (mcpServers):\n` +
|
|
324
|
+
` {\n` +
|
|
325
|
+
` "mcpServers": {\n` +
|
|
326
|
+
` "starmux": {\n` +
|
|
327
|
+
` "type": "streamable-http",\n` +
|
|
328
|
+
` "url": "https://<your-domain-or-tunnel>/mcp",\n` +
|
|
329
|
+
` "headers": {\n` +
|
|
330
|
+
` "Authorization": "Bearer ${result.key}"\n` +
|
|
331
|
+
` }\n` +
|
|
332
|
+
` }\n` +
|
|
333
|
+
` }\n` +
|
|
334
|
+
` }\n`);
|
|
243
335
|
return;
|
|
244
336
|
}
|
|
245
337
|
if (op === 'mode') {
|
|
246
|
-
const
|
|
338
|
+
const raw = positional[1];
|
|
339
|
+
const mode = raw === 'none' ? 'ingress' : raw;
|
|
247
340
|
if (mode !== 'ingress' && mode !== 'api-key' && mode !== 'oauth')
|
|
248
|
-
fail('Choose ingress, api-key or oauth.');
|
|
341
|
+
fail('Choose none (or ingress), api-key or oauth.');
|
|
249
342
|
await access.setMode(mode);
|
|
250
343
|
await changeRegistry(async (r) => { r.instances.find(i => i.id === instance.id).requiredAccess = mode; });
|
|
344
|
+
const label = mode === 'ingress' ? 'none (ingress)' : mode;
|
|
251
345
|
if (json)
|
|
252
346
|
print(await access.snapshot());
|
|
253
347
|
else
|
|
254
|
-
process.stdout.write('Authentication: ' +
|
|
348
|
+
process.stdout.write('Authentication: ' + label + '\n');
|
|
255
349
|
return;
|
|
256
350
|
}
|
|
257
351
|
if (op === 'revoke') {
|
|
@@ -321,15 +415,16 @@ async function logs(instance) {
|
|
|
321
415
|
fail('--lines must be 1..200.');
|
|
322
416
|
const file = path.join(instancePaths(instance).logs, 'core.log');
|
|
323
417
|
let offset = 0;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
418
|
+
const text = await exists(file) ? await readTrusted(file, true, 32 * 1024 * 1024) : '';
|
|
419
|
+
offset = Buffer.byteLength(text);
|
|
420
|
+
const lines = text.split('\n');
|
|
421
|
+
if (text.endsWith('\n'))
|
|
422
|
+
lines.pop();
|
|
423
|
+
const output = lines.slice(-count).join('\n') + (text.endsWith('\n') ? '\n' : '');
|
|
424
|
+
if (json)
|
|
425
|
+
print({ instanceId: instance.id, output });
|
|
426
|
+
else
|
|
427
|
+
process.stdout.write(output || 'No logs yet.\n');
|
|
333
428
|
if (!flags.follow)
|
|
334
429
|
return;
|
|
335
430
|
let done = false;
|
|
@@ -370,6 +465,10 @@ async function logs(instance) {
|
|
|
370
465
|
async function configCommand() {
|
|
371
466
|
const op = positional[0] ?? 'show';
|
|
372
467
|
const selector = string('instance');
|
|
468
|
+
if (op === 'schema') {
|
|
469
|
+
print(configJSONSchema());
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
373
472
|
if (op === 'import') {
|
|
374
473
|
const file = positional[1];
|
|
375
474
|
if (!file)
|
|
@@ -432,6 +531,57 @@ async function configCommand() {
|
|
|
432
531
|
args.push('--approve-trusted-developer', '--approve-unrestricted');
|
|
433
532
|
await handle('config', args);
|
|
434
533
|
}
|
|
534
|
+
function validateSubcommand() {
|
|
535
|
+
if (command !== 'config' && command !== 'auth')
|
|
536
|
+
return;
|
|
537
|
+
const op = positional[0] ?? (command === 'config' ? 'show' : '');
|
|
538
|
+
let name = command + (op ? ' ' + op : '');
|
|
539
|
+
let count = positional.length - (positional.length ? 1 : 0);
|
|
540
|
+
let minimum = 0;
|
|
541
|
+
let maximum = 0;
|
|
542
|
+
let supported;
|
|
543
|
+
if (command === 'config') {
|
|
544
|
+
const commands = { show: ['effective'], path: [], edit: [], validate: [], diff: [], apply: ['revision', 'yes'], schema: [], recover: [],
|
|
545
|
+
import: ['name', 'link', 'yes'], copy: [], rename: [], default: [], archive: [], restore: [], export: ['output'], relink: ['file'], manage: [],
|
|
546
|
+
save: ['file', 'revision', 'yes'], patch: ['patch', 'revision', 'yes'], secret: [] };
|
|
547
|
+
supported = commands[op];
|
|
548
|
+
if (['import', 'copy', 'rename', 'default'].includes(op))
|
|
549
|
+
minimum = maximum = 1;
|
|
550
|
+
if (op === 'relink') {
|
|
551
|
+
minimum = string('file') ? 0 : 1;
|
|
552
|
+
maximum = minimum;
|
|
553
|
+
}
|
|
554
|
+
if (op === 'secret') {
|
|
555
|
+
minimum = maximum = 2;
|
|
556
|
+
if (!['set', 'remove'].includes(positional[1] ?? ''))
|
|
557
|
+
fail('Usage: starmux config secret set|remove NAME');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else if (op === 'oauth') {
|
|
561
|
+
const action = positional[1] ?? 'status';
|
|
562
|
+
name += ' ' + action;
|
|
563
|
+
count = Math.max(0, positional.length - 2);
|
|
564
|
+
const actions = { status: [], doctor: [],
|
|
565
|
+
'init-keycloak': ['resource', 'callback', 'allow-loopback-http'],
|
|
566
|
+
setup: ['issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'allow-loopback-http'] };
|
|
567
|
+
supported = actions[action];
|
|
568
|
+
if (flags.subject !== undefined && flags.subjects !== undefined)
|
|
569
|
+
fail('Use either --subject or --subjects.');
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const commands = { '': [], key: ['expires'], mode: [], revoke: [] };
|
|
573
|
+
supported = commands[op];
|
|
574
|
+
if (['key', 'mode', 'revoke'].includes(op))
|
|
575
|
+
minimum = maximum = 1;
|
|
576
|
+
}
|
|
577
|
+
if (!supported)
|
|
578
|
+
fail('Unknown command: ' + name + '. See starmux ' + command + ' --help.');
|
|
579
|
+
if (count < minimum || count > maximum || positional.slice(1).some(value => !value.trim()))
|
|
580
|
+
fail('Invalid arguments for starmux ' + name + '. See starmux ' + command + ' --help.');
|
|
581
|
+
for (const key of Object.keys(flags))
|
|
582
|
+
if (!['instance', 'json', 'help', 'version'].includes(key) && !supported.includes(key))
|
|
583
|
+
fail('Option --' + key + ' is not supported by ' + name + '.');
|
|
584
|
+
}
|
|
435
585
|
async function main() {
|
|
436
586
|
if (flags.version) {
|
|
437
587
|
process.stdout.write(version + '\n');
|
|
@@ -443,15 +593,20 @@ async function main() {
|
|
|
443
593
|
}
|
|
444
594
|
const allowed = {
|
|
445
595
|
up: ['port', 'read', 'write', 'code', 'yes', 'foreground', 'app-parent-pid'], init: ['port', 'read', 'write', 'code', 'yes'],
|
|
446
|
-
status: ['all'], ls: ['archived'], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'], migrate: ['yes'],
|
|
596
|
+
status: ['all'], ls: ['archived'], rm: [], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'], migrate: ['yes'],
|
|
447
597
|
auth: ['expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'allow-loopback-http'],
|
|
448
598
|
config: ['yes', 'archived', 'effective', 'link', 'name', 'revision', 'patch', 'file', 'output'], permissions: ['target', 'mode'], privacy: ['all', 'target']
|
|
449
599
|
};
|
|
450
600
|
for (const key of Object.keys(flags))
|
|
451
601
|
if (!['instance', 'json', 'help', 'version'].includes(key) && !allowed[command]?.includes(key))
|
|
452
602
|
fail('Option --' + key + ' is not supported by ' + command + '.');
|
|
603
|
+
if (string('instance') !== undefined && !string('instance').trim())
|
|
604
|
+
fail('--instance requires a non-empty name or ID.');
|
|
605
|
+
validateSubcommand();
|
|
453
606
|
if (['status', 'ls', 'down', 'stop', 'logs', 'doctor', 'verify', 'dashboard'].includes(command) && positional.length)
|
|
454
607
|
fail('Unexpected argument: ' + positional[0]);
|
|
608
|
+
if (command === 'rm' && positional.length > 1)
|
|
609
|
+
fail('Usage: starmux rm [NAME]');
|
|
455
610
|
if (command === 'auth' && ['key', 'mode', 'revoke'].includes(positional[0] ?? '') && positional.length !== 2)
|
|
456
611
|
fail('Usage: starmux auth ' + positional[0] + ' VALUE');
|
|
457
612
|
if (command === 'migrate' && positional.length > 1)
|
|
@@ -469,8 +624,10 @@ async function main() {
|
|
|
469
624
|
await start(instance);
|
|
470
625
|
else if (json)
|
|
471
626
|
print({ result: 'SAVED', instance });
|
|
472
|
-
else
|
|
473
|
-
|
|
627
|
+
else {
|
|
628
|
+
const flag = await instanceFlag(instance);
|
|
629
|
+
process.stdout.write(`Saved ${instance.name}. Start with starmux up${flag ? ' ' + flag : ''}\n`);
|
|
630
|
+
}
|
|
474
631
|
return;
|
|
475
632
|
}
|
|
476
633
|
if (command === 'migrate') {
|
|
@@ -481,6 +638,11 @@ async function main() {
|
|
|
481
638
|
await handle('instance', ['list', ...(flags.archived ? ['--archived'] : []), ...(json ? ['--json'] : [])]);
|
|
482
639
|
return;
|
|
483
640
|
}
|
|
641
|
+
if (command === 'rm') {
|
|
642
|
+
const target = positional[0] ?? string('instance') ?? (await selected()).id;
|
|
643
|
+
await handle('instance', ['archive', target, ...(json ? ['--json'] : [])]);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
484
646
|
if (command === 'config') {
|
|
485
647
|
await configCommand();
|
|
486
648
|
return;
|
|
@@ -525,8 +687,19 @@ async function main() {
|
|
|
525
687
|
fail('Configured port is in use: ' + loaded.config.port);
|
|
526
688
|
if (json)
|
|
527
689
|
print({ result: 'PASS', node: process.version, instanceId: instance.id, configState: state.configState });
|
|
528
|
-
else
|
|
529
|
-
|
|
690
|
+
else {
|
|
691
|
+
const isDef = await isDefaultInstance(instance);
|
|
692
|
+
const access = await (await createAccessControl(instancePaths(instance).access, { initialize: false })).snapshot();
|
|
693
|
+
const modeLabel = loaded.config.accessRole === 'trusted' ? 'Coding mode' : 'Read-only mode';
|
|
694
|
+
const authLabel = access.mode === 'ingress' ? 'None (no token required)' : access.mode === 'api-key' ? `API key (${access.credentials.filter(c => c.status === 'active').length} active)` : 'OAuth';
|
|
695
|
+
const portState = state.phase === 'running' ? `Running on 127.0.0.1:${state.port}` : `Stopped · port ${loaded.config.port} available`;
|
|
696
|
+
process.stdout.write(`✓ Runtime Node ${process.version} & Desktop Commander engine ready\n` +
|
|
697
|
+
`✓ Service ${portState}${isDef ? '' : ` (instance: ${instance.name})`}\n` +
|
|
698
|
+
`✓ Workspace ${loaded.config.projectRoot} (${modeLabel})\n` +
|
|
699
|
+
`✓ Configuration ${instance.source.path} (${state.configState})\n` +
|
|
700
|
+
`✓ Authentication ${authLabel}\n` +
|
|
701
|
+
`PASS · Node, dependencies, workspace, configuration, port and authentication.\n`);
|
|
702
|
+
}
|
|
530
703
|
}
|
|
531
704
|
else {
|
|
532
705
|
const args = [...positional, '--instance', instance.id];
|