@aifabrix/builder 2.57.0 → 2.59.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/lib/agent-kit/codex-app-server.js +44 -0
- package/lib/agent-kit/init.js +4 -17
- package/lib/agent-kit/session-report.js +15 -10
- package/lib/agent-kit/setup-auth.js +131 -0
- package/lib/agent-kit/setup-context.js +118 -0
- package/lib/agent-kit/setup-github.js +70 -0
- package/lib/agent-kit/setup-managed.js +79 -0
- package/lib/agent-kit/setup-process.js +42 -0
- package/lib/agent-kit/setup-session.js +53 -0
- package/lib/agent-kit/setup-state.js +52 -0
- package/lib/agent-kit/setup-workspace.js +113 -0
- package/lib/agent-kit/setup.js +84 -0
- package/lib/agent-kit/start.js +4 -3
- package/lib/agent-kit/tmux-sessions.js +20 -3
- package/lib/api/dev.api.js +1 -1
- package/lib/api/miso-health.api.js +26 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/app/deploy.js +0 -102
- package/lib/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +53 -2
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-environment.js +17 -4
- package/lib/cli/setup-help.js +86 -0
- package/lib/cli/setup-onboarding.js +37 -0
- package/lib/cli/setup-utility-repair.js +7 -5
- package/lib/commands/agent-kit-help.js +21 -7
- package/lib/commands/agent-kit-sessions.js +126 -0
- package/lib/commands/agent-kit-setup.js +44 -0
- package/lib/commands/agent-kit.js +5 -79
- package/lib/commands/dev-init.js +21 -23
- package/lib/commands/dev-refresh.js +2 -2
- package/lib/commands/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -2
- package/lib/commands/governance-verify-external.js +12 -5
- package/lib/commands/repair-datasource.js +9 -9
- package/lib/commands/repair.js +45 -8
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/upload-qualification-client.js +56 -11
- package/lib/commands/upload-qualification-poll.js +51 -22
- package/lib/commands/wizard-dataplane.js +16 -52
- package/lib/core/config-attach-extensions.js +4 -1
- package/lib/datasource/datasource-validate-summary.js +8 -5
- package/lib/datasource/sync-defaults.js +53 -0
- package/lib/deployment/installation/azure-infra-stage.js +50 -9
- package/lib/deployment/installation/azure-preflight.js +63 -7
- package/lib/deployment/installation/azure-readiness.js +59 -7
- package/lib/deployment/installation/environment-auth.js +60 -20
- package/lib/deployment/installation/environment-stage.js +9 -5
- package/lib/deployment/installation/infra-catalog.js +57 -5
- package/lib/deployment/installation/infra-manifest.js +22 -2
- package/lib/deployment/installation/infra-projection.js +14 -3
- package/lib/deployment/installation/registry-auth-mode.js +121 -0
- package/lib/deployment/installation/stage-input.js +21 -6
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +8 -9
- package/lib/generator/index.js +23 -12
- package/lib/integration-definition/apply.js +2 -2
- package/lib/internal/fs-real-sync.js +4 -1
- package/lib/programmatic/bearer-auth.js +3 -62
- package/lib/programmatic/builder-api-startup.js +51 -0
- package/lib/programmatic/builder-miso-runtime.js +167 -0
- package/lib/programmatic/openapi-descriptions.js +3 -3
- package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
- package/lib/programmatic/openapi-spec.js +2 -1
- package/lib/programmatic/route-handler-map.js +3 -2
- package/lib/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +1 -1
- package/lib/schema/infra.parameter.yaml +2 -1
- package/lib/schema/infrastructure-schema.json +200 -3
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/app-register-config.js +2 -4
- package/lib/utils/build-resolve-image.js +2 -21
- package/lib/utils/cli-utils.js +8 -1
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +113 -0
- package/lib/utils/dev-init-ssh-merge.js +5 -4
- package/lib/utils/dev-user-groups.js +2 -1
- package/lib/utils/docker-build.js +53 -19
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/env-map.js +23 -6
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/image-tags.js +88 -0
- package/lib/utils/promote-dev-docker-image.js +6 -0
- package/lib/utils/registry-credentials.js +236 -0
- package/lib/utils/resolve-docker-image-ref.js +78 -9
- package/lib/utils/secrets-helpers.js +9 -1
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +2 -4
- package/package.json +4 -2
- package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
- package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
- package/templates/agent-kit/skills/shared/hosts.md +23 -5
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +440 -82
|
@@ -16,6 +16,7 @@ Coding hosts (--host) are Cursor, Claude Code, and/or Codex in this workspace.
|
|
|
16
16
|
GitHub remotes are a separate command (agent-kit remote create).
|
|
17
17
|
|
|
18
18
|
Examples:
|
|
19
|
+
$ aifabrix agent-kit setup https://controller.example.com
|
|
19
20
|
$ aifabrix agent-kit init cust-demo --host all --approve-all --yes
|
|
20
21
|
$ aifabrix agent-kit install --host claude --approve-all --yes
|
|
21
22
|
$ aifabrix agent-kit install --host all --approve-all claude --yes
|
|
@@ -94,10 +95,12 @@ Silent remote: --host all --remote --org <owner> --yes
|
|
|
94
95
|
const REMOTE_GROUP_HELP = `
|
|
95
96
|
This is GitHub only: create origin and push this workspace.
|
|
96
97
|
It does not choose Cursor, Claude Code, or Codex.
|
|
98
|
+
For an existing GitHub repository on a managed host, use af onboarding --repo owner/name.
|
|
97
99
|
|
|
98
100
|
Examples:
|
|
99
|
-
$
|
|
100
|
-
$
|
|
101
|
+
$ cd ~/agent-kit-workspace && af agent-kit remote create --org my-org --name my-repo --yes
|
|
102
|
+
$ af onboarding --repo my-org/existing-repo
|
|
103
|
+
$ af onboarding --repo my-org/new-repo
|
|
101
104
|
$ aifabrix agent-kit install --host claude --approve-all --yes
|
|
102
105
|
$ aifabrix agent-kit install --host codex --approve-all --yes
|
|
103
106
|
$ aifabrix agent-kit install --host all --approve-all claude --yes
|
|
@@ -105,12 +108,16 @@ Examples:
|
|
|
105
108
|
|
|
106
109
|
const REMOTE_HELP = `
|
|
107
110
|
Creates a GitHub repository (origin) and pushes this workspace.
|
|
111
|
+
Run inside the local workspace. --name sets the new GitHub repository name;
|
|
112
|
+
omit it to use the workspace folder name. This command does not clone an
|
|
113
|
+
existing GitHub repository.
|
|
108
114
|
Not coding-host selection — use --host on install/update for Claude or Codex.
|
|
109
115
|
|
|
110
116
|
Examples:
|
|
111
|
-
$ cd
|
|
112
|
-
$
|
|
113
|
-
$
|
|
117
|
+
$ cd ~/agent-kit-workspace && af agent-kit remote create --org my-org --name my-repo --yes
|
|
118
|
+
$ af agent-kit remote create --org my-org --name my-repo --dry-run --json
|
|
119
|
+
$ af onboarding --repo my-org/existing-repo
|
|
120
|
+
$ af onboarding --repo my-org/new-repo
|
|
114
121
|
|
|
115
122
|
Interactive (TTY): omit flags to be prompted. Silent: --org <owner> --yes
|
|
116
123
|
`;
|
|
@@ -134,7 +141,10 @@ Examples:
|
|
|
134
141
|
`;
|
|
135
142
|
|
|
136
143
|
const LIST_HELP = `
|
|
137
|
-
Lists
|
|
144
|
+
Lists active tmux sessions and direct Codex app servers owned by the current
|
|
145
|
+
Unix user. On a terminal, choose a tmux session to attach or Close. Codex app
|
|
146
|
+
servers are shown as information and cannot be selected from this menu.
|
|
147
|
+
--json and non-interactive output print the report without a prompt.
|
|
138
148
|
|
|
139
149
|
Examples:
|
|
140
150
|
$ aifabrix agent-kit list
|
|
@@ -142,9 +152,13 @@ Examples:
|
|
|
142
152
|
`;
|
|
143
153
|
|
|
144
154
|
const KILL_HELP = `
|
|
145
|
-
Stops exactly one tmux session.
|
|
155
|
+
Stops exactly one tmux session by name. Omit the name in an interactive
|
|
156
|
+
terminal to choose a tmux session or Codex app server. Stopping a Codex app
|
|
157
|
+
server requires confirmation and may disconnect an active desktop chat.
|
|
158
|
+
Pass an exact tmux name for scripts or --json.
|
|
146
159
|
|
|
147
160
|
Examples:
|
|
161
|
+
$ aifabrix agent-kit kill
|
|
148
162
|
$ aifabrix agent-kit kill aifabrix-miso
|
|
149
163
|
$ aifabrix agent-kit kill aifabrix-miso --json
|
|
150
164
|
`;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** CLI actions for listing, attaching, and stopping Agent Kit sessions. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
const { handleCommandError } = require('../utils/cli-utils');
|
|
7
|
+
const { formatSuccessLine } = require('../utils/cli-layout-chalk');
|
|
8
|
+
const { runList, runKill, runAttach } = require('../agent-kit/tmux-sessions');
|
|
9
|
+
const { listCodexAppServers, stopCodexAppServer } = require('../agent-kit/codex-app-server');
|
|
10
|
+
const { formatSessionList, formatMissingKillName } = require('../agent-kit/session-report');
|
|
11
|
+
const { LIST_HELP, KILL_HELP } = require('./agent-kit-help');
|
|
12
|
+
|
|
13
|
+
function emitListReport(report, asJson) {
|
|
14
|
+
logger.log(asJson ? JSON.stringify(report, null, 2) : formatSessionList(report));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function emitKillReport(report, asJson) {
|
|
18
|
+
logger.log(asJson ? JSON.stringify(report, null, 2)
|
|
19
|
+
: formatSuccessLine(report.kind === 'codex-app-server'
|
|
20
|
+
? `Sent stop signal to Codex app server PID ${report.pid}`
|
|
21
|
+
: `Stopped tmux session ${report.session}`));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listSessions(options = {}) {
|
|
25
|
+
const report = (options.runList || runList)(options);
|
|
26
|
+
const codexServers = (options.listCodexAppServers || listCodexAppServers)(options);
|
|
27
|
+
return { ...report, codexServers, serverCount: codexServers.length };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runListLeaf(options = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const report = listSessions(options);
|
|
33
|
+
emitListReport(report, Boolean(options.json));
|
|
34
|
+
const tty = options.isTTY === undefined
|
|
35
|
+
? Boolean(process.stdin.isTTY && process.stdout.isTTY) : options.isTTY;
|
|
36
|
+
if (!tty || options.json || report.sessions.length === 0) return;
|
|
37
|
+
const choices = report.sessions.map(session => ({
|
|
38
|
+
name: `${session.name} (${session.attached ? 'attached' : 'detached'})`, value: session.name
|
|
39
|
+
}));
|
|
40
|
+
choices.push({ name: 'Close', value: null });
|
|
41
|
+
const answer = await (options.prompt || inquirer.prompt)([{
|
|
42
|
+
type: 'list', name: 'name', message: 'Attach to a tmux session:', choices
|
|
43
|
+
}]);
|
|
44
|
+
if (answer.name) (options.runAttach || runAttach)({ ...options, name: answer.name });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
handleCommandError(error, 'agent-kit list');
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function selectKillSession(options = {}) {
|
|
52
|
+
const tty = options.isTTY === undefined
|
|
53
|
+
? Boolean(process.stdin.isTTY && process.stdout.isTTY) : options.isTTY;
|
|
54
|
+
if (!tty || options.json) return { state: 'missing' };
|
|
55
|
+
const report = listSessions(options);
|
|
56
|
+
emitListReport(report, false);
|
|
57
|
+
if (report.sessions.length + report.codexServers.length === 0) return { state: 'empty' };
|
|
58
|
+
const choices = report.sessions.map(session => ({
|
|
59
|
+
name: `tmux: ${session.name} (${session.attached ? 'attached' : 'detached'})`,
|
|
60
|
+
value: { kind: 'tmux', name: session.name }
|
|
61
|
+
}));
|
|
62
|
+
report.codexServers.forEach(server => choices.push({
|
|
63
|
+
name: `Codex app server (PID ${server.pid})`,
|
|
64
|
+
value: { kind: 'codex-app-server', ...server }
|
|
65
|
+
}));
|
|
66
|
+
choices.push({ name: 'Cancel', value: null });
|
|
67
|
+
const answer = await (options.prompt || inquirer.prompt)([{
|
|
68
|
+
type: 'list', name: 'selection', message: 'Select one session to stop:', choices
|
|
69
|
+
}]);
|
|
70
|
+
if (!answer.selection) return { state: 'cancelled' };
|
|
71
|
+
if (answer.selection.kind === 'codex-app-server') {
|
|
72
|
+
const confirm = await (options.prompt || inquirer.prompt)([{
|
|
73
|
+
type: 'confirm', name: 'stop', default: false,
|
|
74
|
+
message: `Stop Codex app server PID ${answer.selection.pid}? This may disconnect this chat; the client may reconnect.`
|
|
75
|
+
}]);
|
|
76
|
+
if (!confirm.stop) return { state: 'cancelled' };
|
|
77
|
+
}
|
|
78
|
+
return { state: 'selected', ...answer.selection };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function addListCommand(group) {
|
|
82
|
+
group.command('list')
|
|
83
|
+
.description('List sessions and choose a tmux session to attach')
|
|
84
|
+
.option('--json', 'Machine-readable output')
|
|
85
|
+
.addHelpText('after', LIST_HELP)
|
|
86
|
+
.action(runListLeaf);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function addKillCommand(group) {
|
|
90
|
+
group.command('kill [name]')
|
|
91
|
+
.description('Stop a tmux session by name, or choose a session interactively')
|
|
92
|
+
.option('--json', 'Machine-readable output')
|
|
93
|
+
.addHelpText('after', KILL_HELP)
|
|
94
|
+
.action(runKillLeaf);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runKillLeaf(name, options = {}) {
|
|
98
|
+
try {
|
|
99
|
+
const selection = name ? { state: 'selected', kind: 'tmux', name } : await selectKillSession(options);
|
|
100
|
+
if (selection.state === 'missing') {
|
|
101
|
+
const report = formatMissingKillName(Boolean(options.json));
|
|
102
|
+
logger.error(report.stderr);
|
|
103
|
+
if (report.stdout) logger.log(report.stdout);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (selection.state === 'empty' || selection.state === 'cancelled') {
|
|
108
|
+
logger.log('No session stopped.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const report = selection.kind === 'codex-app-server'
|
|
112
|
+
? (options.stopCodexAppServer || stopCodexAppServer)(selection, options)
|
|
113
|
+
: (options.runKill || runKill)({ ...options, name: selection.name });
|
|
114
|
+
emitKillReport(report, Boolean(options.json));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
handleCommandError(error, 'agent-kit kill');
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function addSessionCommands(group) {
|
|
122
|
+
addListCommand(group);
|
|
123
|
+
addKillCommand(group);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { addSessionCommands, listSessions, runListLeaf, selectKillSession, runKillLeaf };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Minimal setup command: controller required, local Codex workspace by default. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
const { formatBlockingError } = require('../utils/cli-layout-chalk');
|
|
6
|
+
const { maskSensitiveData } = require('../utils/log-redaction');
|
|
7
|
+
const { runSetup } = require('../agent-kit/setup');
|
|
8
|
+
|
|
9
|
+
const SETUP_HELP = `
|
|
10
|
+
Start onboarding with only a Controller URL. Defaults to Codex and a local Git
|
|
11
|
+
workspace under the AI Fabrix work directory; GitHub is not required.
|
|
12
|
+
If the work directory is unset, setup asks once and saves it.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
$ aifabrix agent-kit setup https://controller.example.com
|
|
16
|
+
$ aifabrix agent-kit setup https://controller.example.com --code claude
|
|
17
|
+
$ aifabrix agent-kit setup https://controller.example.com --repo example-org/example-workspace
|
|
18
|
+
|
|
19
|
+
With --repo owner/name, setup reuses a matching local checkout, clones an accessible
|
|
20
|
+
remote, or creates a private repository and pushes. Unresolved access stops safely.
|
|
21
|
+
Without --repo, files stay local and no GitHub login or push occurs.
|
|
22
|
+
Folder name is the repository name, or workspace without --repo. Change environment with
|
|
23
|
+
aifabrix auth --set-environment <environment>.
|
|
24
|
+
Requires an interactive Linux/macOS terminal, git and tmux (gh only for GitHub).
|
|
25
|
+
Installing a missing Claude CLI through npm requires Node.js 22+.
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
function addSetupCommand(group) {
|
|
29
|
+
group.command('setup <controllerUrl>')
|
|
30
|
+
.description('Onboard a local Codex workspace; optionally use Claude or GitHub')
|
|
31
|
+
.option('--repo <owner/name>', 'GitHub repository to reuse, clone, or create privately')
|
|
32
|
+
.option('--code <agent>', 'Coding agent: codex or claude', 'codex')
|
|
33
|
+
.addHelpText('after', SETUP_HELP)
|
|
34
|
+
.action(async(controllerUrl, options) => {
|
|
35
|
+
try {
|
|
36
|
+
await runSetup({ ...options, controllerUrl });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error(formatBlockingError(maskSensitiveData(error.message)));
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { addSetupCommand, SETUP_HELP };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** `aifabrix agent-kit` command group. */
|
|
2
|
-
|
|
3
2
|
'use strict';
|
|
4
3
|
|
|
5
4
|
const logger = require('../utils/logger');
|
|
@@ -11,8 +10,8 @@ const {
|
|
|
11
10
|
} = require('../utils/cli-layout-chalk');
|
|
12
11
|
const { runInstall, runUpdate } = require('../agent-kit/run');
|
|
13
12
|
const { runStart } = require('../agent-kit/start');
|
|
14
|
-
const {
|
|
15
|
-
const {
|
|
13
|
+
const { addSetupCommand } = require('./agent-kit-setup');
|
|
14
|
+
const { addSessionCommands } = require('./agent-kit-sessions');
|
|
16
15
|
const path = require('path');
|
|
17
16
|
const { runInit, runRemoteCreate } = require('../agent-kit/init');
|
|
18
17
|
const { enrichInitHostOptions, enrichInitRemoteOptions, enrichRemoteOptions, enrichApproveAllOptions } = require('../agent-kit/remote-prompts');
|
|
@@ -33,8 +32,6 @@ const {
|
|
|
33
32
|
REMOTE_GROUP_HELP,
|
|
34
33
|
REMOTE_HELP,
|
|
35
34
|
START_HELP,
|
|
36
|
-
LIST_HELP,
|
|
37
|
-
KILL_HELP,
|
|
38
35
|
HOST_OPTION_HELP,
|
|
39
36
|
APPROVE_ALL_OPTION_HELP
|
|
40
37
|
} = require('./agent-kit-help');
|
|
@@ -131,32 +128,6 @@ function emitStartReport(json, asJson) {
|
|
|
131
128
|
logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Ready'));
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
/**
|
|
135
|
-
* @param {object} json
|
|
136
|
-
* @param {boolean} asJson
|
|
137
|
-
* @returns {void}
|
|
138
|
-
*/
|
|
139
|
-
function emitListReport(json, asJson) {
|
|
140
|
-
if (asJson) {
|
|
141
|
-
logger.log(JSON.stringify(json, null, 2));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
logger.log(formatSessionList(json));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* @param {object} json
|
|
149
|
-
* @param {boolean} asJson
|
|
150
|
-
* @returns {void}
|
|
151
|
-
*/
|
|
152
|
-
function emitKillReport(json, asJson) {
|
|
153
|
-
if (asJson) {
|
|
154
|
-
logger.log(JSON.stringify(json, null, 2));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
logger.log(formatSuccessLine(`Stopped tmux session ${json.session}`));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
131
|
/**
|
|
161
132
|
* @param {Function} runner
|
|
162
133
|
* @param {object} options
|
|
@@ -185,6 +156,7 @@ function setupAgentKitCommands(program) {
|
|
|
185
156
|
.description('Install, init, and update the AI Fabrix Agent Kit in a customer workspace')
|
|
186
157
|
.addHelpText('after', GROUP_HELP);
|
|
187
158
|
|
|
159
|
+
addSetupCommand(group);
|
|
188
160
|
addInitCommand(group);
|
|
189
161
|
addInstallCommand(group);
|
|
190
162
|
addStatusCommand(group);
|
|
@@ -192,8 +164,7 @@ function setupAgentKitCommands(program) {
|
|
|
192
164
|
addValidateCommand(group);
|
|
193
165
|
addRemoteCommand(group);
|
|
194
166
|
addStartCommand(group);
|
|
195
|
-
|
|
196
|
-
addKillCommand(group);
|
|
167
|
+
addSessionCommands(group);
|
|
197
168
|
}
|
|
198
169
|
|
|
199
170
|
/**
|
|
@@ -254,7 +225,7 @@ function addRemoteCommand(group) {
|
|
|
254
225
|
.command('create')
|
|
255
226
|
.description('Create a private GitHub repository (origin) and push this workspace')
|
|
256
227
|
.option('--org <owner>', 'Hosting org or user (required when non-TTY)')
|
|
257
|
-
.option('--name <repo>', '
|
|
228
|
+
.option('--name <repo>', 'New GitHub repository name (default: workspace folder name)')
|
|
258
229
|
.option('--visibility <mode>', 'private or public', 'private')
|
|
259
230
|
.option('--yes', 'Silent: skip TTY confirmation (pair with --org)')
|
|
260
231
|
.option('--dry-run', 'Plan without creating the remote')
|
|
@@ -451,49 +422,4 @@ function addStartCommand(group) {
|
|
|
451
422
|
});
|
|
452
423
|
}
|
|
453
424
|
|
|
454
|
-
/**
|
|
455
|
-
* @param {import('commander').Command} group
|
|
456
|
-
*/
|
|
457
|
-
function addListCommand(group) {
|
|
458
|
-
group
|
|
459
|
-
.command('list')
|
|
460
|
-
.description('List active tmux sessions for the current user')
|
|
461
|
-
.option('--json', 'Machine-readable output')
|
|
462
|
-
.addHelpText('after', LIST_HELP)
|
|
463
|
-
.action(async(options) => {
|
|
464
|
-
try {
|
|
465
|
-
emitListReport(runList(options), Boolean(options.json));
|
|
466
|
-
} catch (error) {
|
|
467
|
-
handleCommandError(error, 'agent-kit list');
|
|
468
|
-
process.exitCode = 1;
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* @param {import('commander').Command} group
|
|
475
|
-
*/
|
|
476
|
-
function addKillCommand(group) {
|
|
477
|
-
group
|
|
478
|
-
.command('kill [name]')
|
|
479
|
-
.description('Stop one tmux session by exact name')
|
|
480
|
-
.option('--json', 'Machine-readable output')
|
|
481
|
-
.addHelpText('after', KILL_HELP)
|
|
482
|
-
.action(async(name, options) => {
|
|
483
|
-
if (!name) {
|
|
484
|
-
const report = formatMissingKillName(Boolean(options.json));
|
|
485
|
-
logger.error(report.stderr);
|
|
486
|
-
if (report.stdout) logger.log(report.stdout);
|
|
487
|
-
process.exitCode = 1;
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
try {
|
|
491
|
-
emitKillReport(runKill({ ...options, name }), Boolean(options.json));
|
|
492
|
-
} catch (error) {
|
|
493
|
-
handleCommandError(error, 'agent-kit kill');
|
|
494
|
-
process.exitCode = 1;
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
425
|
module.exports = { setupAgentKitCommands };
|
package/lib/commands/dev-init.js
CHANGED
|
@@ -29,13 +29,13 @@ const {
|
|
|
29
29
|
isLinuxCaSudoRequiredError
|
|
30
30
|
} = require('../utils/dev-ca-install');
|
|
31
31
|
const { runOptionalHostsSetup } = require('../utils/dev-hosts-helper');
|
|
32
|
-
const { mergeDevSshConfigAfterInit } = require('../utils/dev-init-ssh-merge');
|
|
32
|
+
const { mergeDevSshConfigAfterInit, hostnameFromBaseUrl } = require('../utils/dev-init-ssh-merge');
|
|
33
33
|
const { resolveInitOptions } = require('../utils/dev-init-resolve');
|
|
34
34
|
const {
|
|
35
35
|
isHealthHttpServerError,
|
|
36
36
|
formatEnsureServerTrustedFailure
|
|
37
37
|
} = require('../utils/dev-init-health-messages');
|
|
38
|
-
const { getBadRequestHint
|
|
38
|
+
const { getBadRequestHint } = require('../utils/dev-init-cert-hints');
|
|
39
39
|
const { augmentAndSaveCaPemForRemoteDocker } = require('../utils/dev-ca-docker-augment');
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -249,7 +249,7 @@ async function _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, config
|
|
|
249
249
|
} catch (err) {
|
|
250
250
|
const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
|
|
251
251
|
logger.log(chalk.yellow(' ⚠ Could not register SSH key: ' + msg));
|
|
252
|
-
|
|
252
|
+
throw new Error(`Onboarding incomplete for developer ${devId}: SSH key registration failed. Ask the server administrator to check role access, then retry dev init with a new PIN.`);
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -262,24 +262,23 @@ async function _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, config
|
|
|
262
262
|
* @param {string} [serverCaPem] - Dev root CA for Node TLS
|
|
263
263
|
*/
|
|
264
264
|
async function applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, serverCaPem) {
|
|
265
|
-
const configDir = getConfigDirForPaths();
|
|
266
|
-
if (issueResponse.settings && typeof issueResponse.settings === 'object') {
|
|
267
|
-
await config.mergeRemoteSettings(issueResponse.settings);
|
|
268
|
-
logger.log(chalk.green(' ') + formatSuccessLine('Config updated from server (issue-cert response)'));
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
logger.log(chalk.gray(' Fetching settings...'));
|
|
272
265
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
266
|
+
let settings = issueResponse.settings;
|
|
267
|
+
if (!settings) {
|
|
268
|
+
logger.log(chalk.gray(' Fetching settings...'));
|
|
269
|
+
settings = await devApi.getSettings(baseUrl, issueResponse.certificate, keyPem, serverCaPem);
|
|
275
270
|
}
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) throw new Error('Server settings are missing or invalid.');
|
|
272
|
+
const fresh = { ...settings, 'developer-id': devId, 'remote-server': baseUrl,
|
|
273
|
+
'sync-ssh-user': settings['sync-ssh-user'] || `dev${devId}`,
|
|
274
|
+
'sync-ssh-host': settings['sync-ssh-host'] || hostnameFromBaseUrl(baseUrl) };
|
|
275
|
+
await config.mergeRemoteSettings(fresh);
|
|
278
276
|
logger.log(chalk.green(' ') + formatSuccessLine('Config updated from server'));
|
|
277
|
+
return fresh;
|
|
279
278
|
} catch (err) {
|
|
280
279
|
const msg = err.status === 400 ? getBadRequestHint() : (err.message || String(err));
|
|
281
|
-
logger.log(chalk.yellow(' ⚠ Could not fetch settings
|
|
282
|
-
|
|
280
|
+
logger.log(chalk.yellow(' ⚠ Could not fetch settings: ' + msg));
|
|
281
|
+
throw new Error(`Onboarding incomplete for developer ${devId}: current server settings could not be applied. Saved settings from another developer will not be used. Ask the server administrator to check role access, then retry dev init with a new PIN.`);
|
|
283
282
|
}
|
|
284
283
|
}
|
|
285
284
|
|
|
@@ -317,7 +316,7 @@ async function resolveServerCaPemForTls(baseUrl, options) {
|
|
|
317
316
|
* @param {{ hostAlias: string|null, syncUser: string, syncHost: string|null }} p
|
|
318
317
|
*/
|
|
319
318
|
function logOnboardingFinished({ hostAlias, syncUser, syncHost }) {
|
|
320
|
-
logger.log(formatSuccessParagraph('
|
|
319
|
+
logger.log(formatSuccessParagraph('Certificate, server settings and SSH key registration complete. Access depends on your server permissions.'));
|
|
321
320
|
if (hostAlias) {
|
|
322
321
|
logger.log(
|
|
323
322
|
chalk.gray(' You can also open an SSH session on the builder with ') +
|
|
@@ -367,14 +366,13 @@ async function runDevInit(options) {
|
|
|
367
366
|
);
|
|
368
367
|
await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
|
|
369
368
|
|
|
370
|
-
await applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, serverCaPemForTls || undefined);
|
|
371
|
-
await
|
|
372
|
-
|
|
373
|
-
|
|
369
|
+
const settings = await applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, serverCaPemForTls || undefined);
|
|
370
|
+
await _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId, serverCaPemForTls || undefined);
|
|
371
|
+
if (settings['docker-endpoint']) await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
|
|
372
|
+
settings,
|
|
374
373
|
warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
|
|
375
374
|
});
|
|
376
|
-
await
|
|
377
|
-
const sshInfo = await mergeDevSshConfigAfterInit(baseUrl, devId);
|
|
375
|
+
const sshInfo = await mergeDevSshConfigAfterInit(baseUrl, devId, settings);
|
|
378
376
|
logOnboardingFinished(sshInfo);
|
|
379
377
|
}
|
|
380
378
|
|
|
@@ -82,9 +82,9 @@ async function runCertificateRefresh(auth) {
|
|
|
82
82
|
const issueResponse = await requestCertificate(auth.serverUrl, devId, pin, csrPem, serverCaPem);
|
|
83
83
|
const caPem = mergeCaPemBlocks(serverCaPem, issueResponse.caCertificate, issueResponse.ca);
|
|
84
84
|
await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
|
|
85
|
-
await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem, serverCaPem);
|
|
85
|
+
const settings = await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem, serverCaPem);
|
|
86
86
|
await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
|
|
87
|
-
settings
|
|
87
|
+
settings,
|
|
88
88
|
remoteServerUrl: auth.serverUrl,
|
|
89
89
|
warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
|
|
90
90
|
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aifabrix dev set-registry` / `dev print-registry` logic.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Installation registry preference and host-scoped credential management
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
14
|
+
const { normalizeRegistryHost } = require('../utils/config-registry-preference');
|
|
15
|
+
const registryCredentials = require('../utils/registry-credentials');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reads the whole of stdin (for --password-stdin).
|
|
19
|
+
* @param {NodeJS.ReadableStream} [stream] - Input stream
|
|
20
|
+
* @returns {Promise<string>}
|
|
21
|
+
*/
|
|
22
|
+
function readAllStdin(stream = process.stdin) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
stream.setEncoding('utf8');
|
|
26
|
+
stream.on('data', (chunk) => {
|
|
27
|
+
data += chunk;
|
|
28
|
+
});
|
|
29
|
+
stream.on('end', () => resolve(data));
|
|
30
|
+
stream.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasOption(value) {
|
|
35
|
+
return value !== undefined && value !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertModeOptions(registry, opts) {
|
|
39
|
+
const credentialOption = hasOption(opts.username) || hasOption(opts.password) || opts.passwordStdin === true;
|
|
40
|
+
if (opts.clear && opts.clearCredentials) {
|
|
41
|
+
throw new Error('Use either --clear or --clear-credentials, not both');
|
|
42
|
+
}
|
|
43
|
+
if ((opts.clear || opts.clearCredentials) && credentialOption) {
|
|
44
|
+
throw new Error('--clear and --clear-credentials cannot be combined with --username or a password option');
|
|
45
|
+
}
|
|
46
|
+
if (opts.clear && hasOption(registry)) {
|
|
47
|
+
throw new Error('--clear removes the saved registry and takes no registry argument');
|
|
48
|
+
}
|
|
49
|
+
if (!opts.clear && !(typeof registry === 'string' && registry.trim())) {
|
|
50
|
+
throw new Error('Registry is required (for example: aifabrix dev set-registry myregistry.azurecr.io)');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertCredentialOptions(opts) {
|
|
55
|
+
if (hasOption(opts.password) && opts.passwordStdin === true) {
|
|
56
|
+
throw new Error('Use either --password or --password-stdin, not both');
|
|
57
|
+
}
|
|
58
|
+
const passwordGiven = hasOption(opts.password) || opts.passwordStdin === true;
|
|
59
|
+
if (hasOption(opts.username) !== passwordGiven) {
|
|
60
|
+
throw new Error('--username and a password (--password or --password-stdin) must be supplied together');
|
|
61
|
+
}
|
|
62
|
+
if (hasOption(opts.username) && !String(opts.username).trim()) {
|
|
63
|
+
throw new Error('--username must not be empty');
|
|
64
|
+
}
|
|
65
|
+
if (hasOption(opts.password) && String(opts.password) === '') {
|
|
66
|
+
throw new Error('--password must not be empty');
|
|
67
|
+
}
|
|
68
|
+
return passwordGiven;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function resolvePassword(opts, readStdin) {
|
|
72
|
+
if (opts.passwordStdin !== true) return String(opts.password);
|
|
73
|
+
const password = (await readStdin()).replace(/\r?\n$/, '');
|
|
74
|
+
if (!password) {
|
|
75
|
+
throw new Error('--password-stdin received an empty password');
|
|
76
|
+
}
|
|
77
|
+
return password;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function saveWithCredentials(host, username, password) {
|
|
81
|
+
const saved = await registryCredentials.saveRegistryCredentials(host, username, password);
|
|
82
|
+
try {
|
|
83
|
+
await config.setRegistry(host);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
saved.rollback();
|
|
86
|
+
throw new Error(`Could not save the registry preference; credentials for ${host} were restored (${error.message})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runClearMode(registry, opts) {
|
|
91
|
+
if (opts.clear) {
|
|
92
|
+
await config.clearRegistry();
|
|
93
|
+
logger.log(formatSuccessLine('Registry cleared (application image.registry applies again)'));
|
|
94
|
+
return { action: 'clear', registry: null, credentials: false };
|
|
95
|
+
}
|
|
96
|
+
const host = normalizeRegistryHost(registry);
|
|
97
|
+
const removed = registryCredentials.removeRegistryCredentials(host);
|
|
98
|
+
logger.log(formatSuccessLine(removed ? `Credentials removed for ${host}` : `No stored credentials for ${host}`));
|
|
99
|
+
return { action: 'clear-credentials', registry: host, credentials: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Runs `dev set-registry`.
|
|
104
|
+
* @param {string|undefined} registry - Positional registry
|
|
105
|
+
* @param {Object} opts - Commander options
|
|
106
|
+
* @param {Object} [deps] - `readStdin` override (tests)
|
|
107
|
+
* @returns {Promise<{ action: string, registry: string|null, credentials: boolean }>}
|
|
108
|
+
*/
|
|
109
|
+
async function runSetRegistry(registry, opts = {}, deps = {}) {
|
|
110
|
+
assertModeOptions(registry, opts);
|
|
111
|
+
if (opts.clear || opts.clearCredentials) return runClearMode(registry, opts);
|
|
112
|
+
const host = normalizeRegistryHost(registry);
|
|
113
|
+
const withCredentials = assertCredentialOptions(opts);
|
|
114
|
+
if (!withCredentials) {
|
|
115
|
+
await config.setRegistry(host);
|
|
116
|
+
logger.log(formatSuccessLine(`Registry set to ${host}`));
|
|
117
|
+
return { action: 'set', registry: host, credentials: false };
|
|
118
|
+
}
|
|
119
|
+
const password = await resolvePassword(opts, deps.readStdin || readAllStdin);
|
|
120
|
+
await saveWithCredentials(host, String(opts.username).trim(), password);
|
|
121
|
+
logger.log(formatSuccessLine(`Registry set to ${host}`));
|
|
122
|
+
logger.log(formatSuccessLine(`Credentials saved for ${host} in your local secrets file (encrypted)`));
|
|
123
|
+
if (opts.passwordStdin !== true) {
|
|
124
|
+
logger.log(formatWarningLine('--password can appear in shell history and process listings; prefer --password-stdin'));
|
|
125
|
+
}
|
|
126
|
+
return { action: 'set', registry: host, credentials: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `dev print-registry`: the saved value plus a newline, or an empty line. Stdout only.
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async function runPrintRegistry() {
|
|
134
|
+
const registry = await config.getRegistry();
|
|
135
|
+
process.stdout.write(`${registry || ''}\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Safe credential status label for `dev show` (never decrypts).
|
|
140
|
+
* @param {string|null} registry - Saved registry
|
|
141
|
+
* @returns {string|null}
|
|
142
|
+
*/
|
|
143
|
+
function registryCredentialLabel(registry) {
|
|
144
|
+
if (!registry) return null;
|
|
145
|
+
try {
|
|
146
|
+
const status = registryCredentials.getRegistryCredentialStatus(registry);
|
|
147
|
+
if (status === 'complete') return 'configured';
|
|
148
|
+
if (status === 'absent') return 'not configured';
|
|
149
|
+
return `incomplete (run: aifabrix dev set-registry ${registry} --clear-credentials)`;
|
|
150
|
+
} catch {
|
|
151
|
+
return 'unknown';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { runSetRegistry, runPrintRegistry, registryCredentialLabel, readAllStdin };
|