@aifabrix/builder 2.58.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.
Files changed (84) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/init.js +4 -17
  3. package/lib/agent-kit/session-report.js +15 -10
  4. package/lib/agent-kit/setup-auth.js +131 -0
  5. package/lib/agent-kit/setup-context.js +118 -0
  6. package/lib/agent-kit/setup-github.js +70 -0
  7. package/lib/agent-kit/setup-managed.js +79 -0
  8. package/lib/agent-kit/setup-process.js +42 -0
  9. package/lib/agent-kit/setup-session.js +53 -0
  10. package/lib/agent-kit/setup-state.js +52 -0
  11. package/lib/agent-kit/setup-workspace.js +113 -0
  12. package/lib/agent-kit/setup.js +84 -0
  13. package/lib/agent-kit/start.js +4 -3
  14. package/lib/agent-kit/tmux-sessions.js +20 -3
  15. package/lib/api/dev.api.js +1 -1
  16. package/lib/api/miso-health.api.js +26 -0
  17. package/lib/api/types/dev.types.js +3 -3
  18. package/lib/app/deploy.js +0 -102
  19. package/lib/cli/index.js +4 -0
  20. package/lib/cli/setup-dev-path-commands.js +4 -0
  21. package/lib/cli/setup-dev.js +7 -4
  22. package/lib/cli/setup-help.js +86 -0
  23. package/lib/cli/setup-onboarding.js +37 -0
  24. package/lib/cli/setup-utility-repair.js +7 -5
  25. package/lib/commands/agent-kit-help.js +21 -7
  26. package/lib/commands/agent-kit-sessions.js +126 -0
  27. package/lib/commands/agent-kit-setup.js +44 -0
  28. package/lib/commands/agent-kit.js +5 -79
  29. package/lib/commands/dev-init.js +21 -23
  30. package/lib/commands/dev-refresh.js +2 -2
  31. package/lib/commands/governance-verify-external.js +12 -5
  32. package/lib/commands/repair-datasource.js +9 -9
  33. package/lib/commands/repair.js +45 -8
  34. package/lib/commands/upload-qualification-client.js +56 -11
  35. package/lib/commands/upload-qualification-poll.js +51 -22
  36. package/lib/commands/wizard-dataplane.js +16 -52
  37. package/lib/datasource/datasource-validate-summary.js +8 -5
  38. package/lib/datasource/sync-defaults.js +53 -0
  39. package/lib/deployment/installation/azure-infra-stage.js +10 -0
  40. package/lib/deployment/installation/azure-preflight.js +13 -2
  41. package/lib/deployment/installation/azure-readiness.js +24 -2
  42. package/lib/deployment/installation/environment-auth.js +60 -20
  43. package/lib/deployment/installation/environment-stage.js +9 -5
  44. package/lib/deployment/installation/infra-catalog.js +8 -2
  45. package/lib/deployment/installation/infra-manifest.js +5 -0
  46. package/lib/deployment/installation/registry-auth-mode.js +33 -10
  47. package/lib/deployment/marketplace-environment.js +26 -1
  48. package/lib/generator/index.js +23 -12
  49. package/lib/integration-definition/apply.js +2 -2
  50. package/lib/internal/fs-real-sync.js +4 -1
  51. package/lib/programmatic/bearer-auth.js +3 -62
  52. package/lib/programmatic/builder-api-startup.js +51 -0
  53. package/lib/programmatic/builder-miso-runtime.js +167 -0
  54. package/lib/programmatic/openapi-descriptions.js +3 -3
  55. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  56. package/lib/programmatic/openapi-spec.js +2 -1
  57. package/lib/programmatic/route-handler-map.js +3 -2
  58. package/lib/schema/environment-deploy-request.schema.json +1 -1
  59. package/lib/schema/external-datasource.schema.json +20 -5
  60. package/lib/schema/infra-parameter.schema.json +1 -1
  61. package/lib/schema/infra.parameter.yaml +2 -1
  62. package/lib/schema/infrastructure-schema.json +145 -1
  63. package/lib/schema/installation-environment.schema.json +1 -1
  64. package/lib/schema/wizard-config.schema.json +1 -1
  65. package/lib/utils/config-paths.js +24 -29
  66. package/lib/utils/config-registry-preference.js +13 -6
  67. package/lib/utils/dev-init-ssh-merge.js +5 -4
  68. package/lib/utils/dev-user-groups.js +2 -1
  69. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  70. package/lib/utils/error-formatters/validation-errors.js +25 -5
  71. package/lib/utils/help-builder.js +4 -3
  72. package/lib/utils/token-manager.js +29 -36
  73. package/package.json +3 -2
  74. package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
  75. package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
  76. package/templates/agent-kit/skills/shared/hosts.md +23 -5
  77. package/templates/agent-kit/workspace/README.md +14 -1
  78. package/templates/applications/builder-api/application.yaml +1 -1
  79. package/templates/applications/dataplane/application.yaml +1 -1
  80. package/templates/applications/dataplane/env.template +1 -1
  81. package/templates/applications/miso-controller/application.yaml +1 -1
  82. package/templates/applications/miso-controller/env.template +1 -1
  83. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  84. package/templates/marketplace/main.json +438 -80
@@ -0,0 +1,52 @@
1
+ /** Machine-local onboarding progress, outside the repository and without credentials. */
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('../internal/fs-real-sync');
6
+ const { randomUUID } = require('crypto');
7
+ const { assertDirectory } = require('./setup-context');
8
+
9
+ function readState(file) {
10
+ if (!fs.existsSync(file)) return null;
11
+ if (!fs.lstatSync(file).isFile()) throw new Error('Onboarding state must be a regular file.');
12
+ try {
13
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
14
+ } catch {
15
+ throw new Error('Onboarding state is invalid; preserve the workspace and inspect its state file.');
16
+ }
17
+ }
18
+
19
+ function acquireWorkspace(context) {
20
+ const dir = path.join(context.root, '.aifabrix-onboarding');
21
+ assertDirectory(dir);
22
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
23
+ const file = path.join(dir, `${context.name}.json`);
24
+ const lock = `${file}.lock`;
25
+ let descriptor;
26
+ try {
27
+ descriptor = fs.openSync(lock, 'wx', 0o600);
28
+ } catch {
29
+ throw new Error(`Workspace setup is locked. If no setup is running, remove the stale lock: ${lock}`);
30
+ }
31
+ fs.closeSync(descriptor);
32
+ const release = () => fs.unlinkSync(lock);
33
+ try {
34
+ const state = readState(file);
35
+ if (state && (state.controller !== context.controller || state.fullName !== context.fullName || state.target !== context.target)) {
36
+ throw new Error('This workspace belongs to a different Controller or repository. Use a different AI Fabrix work directory.');
37
+ }
38
+ return { file, release, state: state || { controller: context.controller, fullName: context.fullName, target: context.target } };
39
+ } catch (error) {
40
+ release();
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ function saveProgress(progress, patch) {
46
+ Object.assign(progress.state, patch);
47
+ const temporary = `${progress.file}.${randomUUID()}.tmp`;
48
+ fs.writeFileSync(temporary, `${JSON.stringify(progress.state, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
49
+ fs.renameSync(temporary, progress.file);
50
+ }
51
+
52
+ module.exports = { acquireWorkspace, saveProgress };
@@ -0,0 +1,113 @@
1
+ /** Local workspace preparation and optional GitHub clone/create with safe resumability. */
2
+ 'use strict';
3
+
4
+ const fs = require('../internal/fs-real-sync');
5
+ const { runInit } = require('./init');
6
+ const { runInstall, runUpdate } = require('./run');
7
+ const { resolveGitRoot } = require('./repo-guard');
8
+ const { ensureInitialCommit } = require('./scm/git');
9
+ const { capture, checked } = require('./setup-process');
10
+ const github = require('./setup-github');
11
+ const { saveProgress } = require('./setup-state');
12
+
13
+ function validateExisting(context, progress, options) {
14
+ if (progress.state.initializing || progress.state.cloning) {
15
+ throw new Error('Workspace initialization or clone was interrupted. Inspect the preserved folder and onboarding state before retrying.');
16
+ }
17
+ if (!fs.existsSync(context.target) || fs.readdirSync(context.target).length === 0) return false;
18
+ if (resolveGitRoot(context.target) !== context.target) throw new Error('Workspace must be its own Git repository.');
19
+ if (context.fullName) {
20
+ const origin = github.originIdentity(context.target, options);
21
+ if (origin || !progress.state.initialized) github.assertOrigin(context, options);
22
+ } else if (!progress.state.initialized || github.originIdentity(context.target, options)) {
23
+ throw new Error('Local workspace is not an existing local-only onboarding workspace. No files were changed.');
24
+ }
25
+ return true;
26
+ }
27
+
28
+ function checkKit(result) {
29
+ if (result.conflicts && result.conflicts.length) {
30
+ throw new Error('Agent Kit files have local conflicts. Resolve them with agent-kit status/update before retrying setup.');
31
+ }
32
+ }
33
+
34
+ function initializeLocal(context, progress, options) {
35
+ // Persist the operation first so an interrupted partial init cannot be adopted on retry.
36
+ saveProgress(progress, { initializing: true });
37
+ runInit({ nameOrPath: context.target, host: context.code, noRemote: true, approveAll: false,
38
+ interactive: false, execFileSync: options.execFileSync });
39
+ saveProgress(progress, { initialized: true, initializing: false });
40
+ }
41
+
42
+ function finishNewRemote(context, progress, options) {
43
+ const opts = { ...options, cwd: context.target };
44
+ if (!progress.state.initialCommit) {
45
+ ensureInitialCommit(context.target, opts);
46
+ const initialCommit = String(capture('git', ['rev-parse', 'HEAD'], opts)).trim();
47
+ saveProgress(progress, { initialCommit });
48
+ }
49
+ if (!progress.state.remoteCreated) {
50
+ github.createRepository(context.fullName, options);
51
+ saveProgress(progress, { remoteCreated: true });
52
+ }
53
+ if (!github.originIdentity(context.target, options)) {
54
+ checked('git', ['remote', 'add', 'origin', `https://github.com/${context.fullName}.git`],
55
+ 'Could not configure the GitHub origin. Local files are preserved.', opts);
56
+ }
57
+ github.pushInitialCommit(context, progress, saveProgress, options);
58
+ }
59
+
60
+ function cloneWorkspace(context, progress, options) {
61
+ if (progress.state.initialized) throw new Error('Previously initialized workspace is missing. Restore it before retrying.');
62
+ saveProgress(progress, { cloning: true });
63
+ github.cloneRepository(context, options);
64
+ saveProgress(progress, { initialized: true, cloning: false });
65
+ }
66
+
67
+ function prepareGithub(context, progress, options, existing) {
68
+ if (existing && github.originIdentity(context.target, options)) {
69
+ if (progress.state.remoteCreated && !progress.state.pushed) finishNewRemote(context, progress, options);
70
+ return false;
71
+ }
72
+ const remote = github.lookupRepository(context.fullName, options);
73
+ if (!remote) {
74
+ if (progress.state.remoteCreated) {
75
+ throw new Error('The existing remote is no longer accessible. Restore access before retrying.');
76
+ }
77
+ if (existing && !progress.state.initialCommit) {
78
+ throw new Error('Initialization stopped before recording the initial commit. Inspect the preserved workspace before creating its remote.');
79
+ }
80
+ if (!existing) initializeLocal(context, progress, options);
81
+ finishNewRemote(context, progress, options);
82
+ return true;
83
+ }
84
+ if (!existing) {
85
+ cloneWorkspace(context, progress, options);
86
+ } else if (!progress.state.remoteCreated) {
87
+ throw new Error('Repository appeared during creation. Inspect the local workspace and remote before connecting them.');
88
+ }
89
+ if (progress.state.remoteCreated && !progress.state.pushed) finishNewRemote(context, progress, options);
90
+ github.assertOrigin(context, options);
91
+ return false;
92
+ }
93
+
94
+ function prepareWorkspace(context, progress, options = {}) {
95
+ const existing = validateExisting(context, progress, options);
96
+ let installed = false;
97
+ if (context.fullName) installed = prepareGithub(context, progress, options, existing);
98
+ else if (!existing) {
99
+ if (progress.state.initialized) throw new Error('Previously initialized workspace is missing. Restore it before retrying.');
100
+ initializeLocal(context, progress, options);
101
+ installed = true;
102
+ }
103
+ if (!installed) {
104
+ const install = options.managed ? runUpdate : runInstall;
105
+ const preview = install({ cwd: context.target, host: context.code, approveAll: false, dryRun: true });
106
+ checkKit(preview);
107
+ checkKit(install({ cwd: context.target, host: context.code, approveAll: false }));
108
+ }
109
+ if (!fs.existsSync(context.target)) throw new Error('Workspace preparation did not create its directory.');
110
+ saveProgress(progress, { initialized: true, ready: true });
111
+ }
112
+
113
+ module.exports = { validateExisting, prepareWorkspace, checkKit };
@@ -0,0 +1,84 @@
1
+ /** Portable local-first agent onboarding. */
2
+ 'use strict';
3
+
4
+ const { randomUUID } = require('crypto');
5
+ const logger = require('../utils/logger');
6
+ const { formatSuccessLine } = require('../utils/cli-layout-chalk');
7
+ const { auditLog } = require('../core/audit-logger');
8
+ const { normalizeSetup, preflight, resolveWork } = require('./setup-context');
9
+ const { acquireWorkspace } = require('./setup-state');
10
+ const { ensureController, ensureGithubLogin, ensureProvider } = require('./setup-auth');
11
+ const { prepareWorkspace, validateExisting } = require('./setup-workspace');
12
+ const { prepareSession, attachSession } = require('./setup-session');
13
+ const { managedSetupOptions } = require('./setup-managed');
14
+
15
+ async function authenticate(context, options, setPhase) {
16
+ setPhase('controller-login');
17
+ logger.log('Connecting to AI Fabrix...');
18
+ await ensureController(context);
19
+ if (context.fullName) {
20
+ setPhase('github-login');
21
+ ensureGithubLogin(options);
22
+ }
23
+ if (context.code === 'cursor') return null;
24
+ setPhase('agent-login');
25
+ return ensureProvider(context, options);
26
+ }
27
+
28
+ async function prepareOnboarding(context, progress, options) {
29
+ const correlationId = randomUUID();
30
+ let provider;
31
+ let session;
32
+ let phase = 'workspace-check';
33
+ try {
34
+ validateExisting(context, progress, options);
35
+ provider = await authenticate(context, options, value => {
36
+ phase = value;
37
+ });
38
+ phase = 'workspace';
39
+ prepareWorkspace(context, progress, options);
40
+ if (!options.managed || context.code === 'claude') {
41
+ phase = 'agent-start';
42
+ session = prepareSession(context, provider, options);
43
+ }
44
+ await auditLog('AUDIT', 'Agent workspace onboarding ready', { correlationId, path: context.target, agent: context.code });
45
+ } catch (error) {
46
+ await auditLog('ERROR', 'Agent workspace onboarding failed', { correlationId, phase });
47
+ throw error;
48
+ } finally {
49
+ progress.release();
50
+ }
51
+ return { session, provider };
52
+ }
53
+
54
+ async function runSetup(options = {}) {
55
+ const input = normalizeSetup(options);
56
+ preflight(input, options);
57
+ const context = await resolveWork(input, options);
58
+ const progress = acquireWorkspace(context);
59
+ const { session, provider } = await prepareOnboarding(context, progress, options);
60
+ logger.log(formatSuccessLine(`Workspace ready: ${context.target}`));
61
+ if (options.managed) {
62
+ const host = options.remoteHost || new URL(context.controller).hostname;
63
+ if (context.code === 'claude') {
64
+ logger.log(`Claude Code running in tmux session ${session}. Attach: tmux attach -t ${session}`);
65
+ logger.log(`Next: Open Claude Code Remote Control on ${host}, or attach to ${session}.`);
66
+ return { path: context.target, code: context.code, session };
67
+ }
68
+ const next = {
69
+ cursor: `In Cursor, connect with Remote SSH to ${host}, then open ${context.target}.`,
70
+ codex: `In Codex, add the SSH project ${context.target} on ${host}.`
71
+ }[context.code];
72
+ logger.log(`Next: ${next} Say "Hi" to begin.`);
73
+ } else {
74
+ attachSession(session, provider, options);
75
+ }
76
+ return { path: context.target, code: context.code, session };
77
+ }
78
+
79
+ async function runManagedOnboarding(options = {}) {
80
+ const managed = await managedSetupOptions(options);
81
+ return runSetup({ ...options, ...managed });
82
+ }
83
+
84
+ module.exports = { runSetup, runManagedOnboarding };
@@ -129,7 +129,7 @@ function buildTmuxSpec(p) {
129
129
  const remote = p.app === 'claude' && p.remoteControl;
130
130
  return {
131
131
  bin: 'tmux',
132
- args: [...mode, spec.bin, ...extra],
132
+ args: [...mode, ...(p.binary ? ['env', p.binary] : [spec.bin]), ...extra],
133
133
  session: p.session,
134
134
  appBin: spec.bin,
135
135
  needTmux: true,
@@ -287,16 +287,17 @@ function resolveStartContext(options) {
287
287
  const cwd = resolveGitRoot(options.cwd || process.cwd());
288
288
  const session = resolveSessionName(options.name, cwd);
289
289
  const remoteControlPrefix = hostUserPrefix(options);
290
- const remoteControlName = resolveRemoteControlName(remoteControlPrefix, session);
290
+ const remoteControlName = options.remoteControlName || resolveRemoteControlName(remoteControlPrefix, session);
291
291
  const remoteControl = options.remoteControl !== false;
292
292
  const spec = buildLaunchSpec({
293
293
  app, cwd, session, remoteControlPrefix, remoteControlName,
294
- detach: Boolean(options.detach), remoteControl
294
+ detach: Boolean(options.detach), remoteControl, binary: options.binary
295
295
  });
296
296
  spec.cwd = cwd;
297
297
  const remoteSpec = app === 'codex' && remoteControl
298
298
  ? buildCodexRemoteSpec(cwd)
299
299
  : undefined;
300
+ if (remoteSpec && options.binary) remoteSpec.bin = options.binary;
300
301
  return { app, cwd, session, remoteControl, spec, remoteSpec };
301
302
  }
302
303
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { execFileSync } = require('child_process');
8
+ const { execFileSync, spawnSync } = require('child_process');
9
9
 
10
10
  const LIST_FORMAT = [
11
11
  '#{session_name}',
@@ -66,7 +66,7 @@ function isNoTmuxServer(error) {
66
66
  .filter(Boolean)
67
67
  .map(String)
68
68
  .join(' ');
69
- return /no server running|failed to connect to server/i.test(message);
69
+ return /no server running|failed to connect to server|error connecting to .*\/tmux-\d+\/.*No such file or directory/i.test(message);
70
70
  }
71
71
 
72
72
  /**
@@ -150,6 +150,22 @@ function runKill(options) {
150
150
  return { command: 'kill', session, killed: true };
151
151
  }
152
152
 
153
+ function runAttach(options) {
154
+ const exec = options.execFileSync || execFileSync;
155
+ const session = normalizeExactSessionName(options.name);
156
+ requireTmux(exec);
157
+ if (!tmuxSessionExists(session, exec)) {
158
+ throw new Error(`tmux session "${session}" is not running. Run: aifabrix agent-kit list`);
159
+ }
160
+ const insideTmux = Boolean((options.env || process.env).TMUX);
161
+ const args = [insideTmux ? 'switch-client' : 'attach-session', '-t', `=${session}`];
162
+ const result = (options.spawnSync || spawnSync)('tmux', args, { stdio: 'inherit' });
163
+ if (!result || result.error || result.signal || result.status !== 0) {
164
+ throw new Error(`Could not attach to tmux session "${session}".`);
165
+ }
166
+ return { command: 'attach', session, attached: true };
167
+ }
168
+
153
169
  module.exports = {
154
170
  LIST_FORMAT,
155
171
  hasOnPath,
@@ -159,5 +175,6 @@ module.exports = {
159
175
  tmuxSessionExists,
160
176
  parseSessionLine,
161
177
  runList,
162
- runKill
178
+ runKill,
179
+ runAttach
163
180
  };
@@ -198,7 +198,7 @@ async function listUsers(serverUrl, clientCertPem, serverCaPem) {
198
198
  * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
199
199
  * @param {string} serverUrl - Builder Server base URL
200
200
  * @param {string} clientCertPem - PEM client certificate
201
- * @param {Object} body - CreateUserDto: developerId, name, email, optional groups (admin | secret-manager | secret-reader | developer | docker)
201
+ * @param {Object} body - CreateUserDto: developerId, name, email, optional groups (admin | secret-manager | secret-reader | developer | docker | integrator)
202
202
  * @param {string} [serverCaPem] - Dev root CA PEM
203
203
  * @returns {Promise<Object>} UserResponseDto
204
204
  */
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Application-token health proof through the public Miso SDK transport.
3
+ * @fileoverview Never reads or returns the application token itself.
4
+ */
5
+ 'use strict';
6
+
7
+ /**
8
+ * POST /api/v1/auth/validate-client-token
9
+ * @requiresPermission {Controller} Client token
10
+ * @param {Object} client Initialized runtime client
11
+ * @param {AbortSignal} signal Probe cancellation
12
+ * @returns {Promise<void>} Resolves only for an authenticated application
13
+ */
14
+ async function validateMisoApplicationToken(client, signal) {
15
+ const response = await client.requestWithAuthStrategy(
16
+ 'POST', '/api/v1/auth/validate-client-token', { methods: ['client-token'] },
17
+ {}, { signal, timeout: 3000, maxRedirects: 0 }
18
+ );
19
+ if (response?.success === false || response?.data?.authenticated !== true) {
20
+ const error = new Error('Miso application authentication failed');
21
+ error.statusCode = 401;
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ module.exports = { validateMisoApplicationToken };
@@ -30,7 +30,7 @@
30
30
  * @property {string} aifabrix-secrets - Local filesystem path or https URL for shared secrets (file vs remote API)
31
31
  * @property {string} aifabrix-env-config - Env config path
32
32
  * @property {string} remote-server - Builder-server base URL
33
- * @property {string} docker-endpoint - Docker API endpoint
33
+ * @property {string} [docker-endpoint] - Docker API endpoint; omit for SSH-only/integrator users
34
34
  * @property {string} [developer-root-domain] - App hostname root (e.g. aifabrix.dev → dev01.aifabrix.dev)
35
35
  * @property {boolean} [docker-tls-skip-verify] - When true, Docker uses DOCKER_TLS_VERIFY=0 even if ca.pem exists (client mTLS unchanged)
36
36
  * @property {string} sync-ssh-user - SSH user for Mutagen
@@ -46,7 +46,7 @@
46
46
  * @property {string} createdAt - ISO 8601
47
47
  * @property {boolean} certificateIssued - Whether cert was issued
48
48
  * @property {string} [certificateValidNotAfter] - Cert validity end (optional)
49
- * @property {string[]} groups - Access groups (admin, secret-manager, secret-reader, developer, docker)
49
+ * @property {string[]} groups - Access groups (admin, secret-manager, secret-reader, developer, docker, integrator)
50
50
  */
51
51
 
52
52
  /**
@@ -55,7 +55,7 @@
55
55
  * @property {string} developerId - Unique developer ID (numeric string)
56
56
  * @property {string} name - Display name
57
57
  * @property {string} email - Email
58
- * @property {string[]} [groups] - Default [developer]; tokens: admin, secret-manager, secret-reader, developer, docker
58
+ * @property {string[]} [groups] - Default [developer]; tokens: admin, secret-manager, secret-reader, developer, docker, integrator
59
59
  */
60
60
 
61
61
  /**
package/lib/app/deploy.js CHANGED
@@ -18,7 +18,6 @@ const {
18
18
 
19
19
  const fs = require('fs').promises;
20
20
  const chalk = require('chalk');
21
- const pushUtils = require('../deployment/push');
22
21
 
23
22
  const SEP = chalk.gray('────────────────────────────────────────');
24
23
  const logger = require('../utils/logger');
@@ -56,106 +55,6 @@ function validateAppName(appName) {
56
55
  }
57
56
  }
58
57
 
59
- /**
60
- * Validates push prerequisites
61
- * @async
62
- * @function validatePushPrerequisites
63
- * @param {string} appName - Application name
64
- * @param {string} registry - Registry URL
65
- * @throws {Error} If prerequisites are not met
66
- */
67
- async function validatePushPrerequisites(appName, registry) {
68
- if (!pushUtils.validateRegistryURL(registry)) {
69
- throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
70
- }
71
-
72
- if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
73
- throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
74
- }
75
-
76
- if (!await pushUtils.checkAzureCLIInstalled()) {
77
- throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
78
- }
79
- }
80
-
81
- /**
82
- * Executes push operations
83
- * @async
84
- * @function executePush
85
- * @param {string} appName - Application name
86
- * @param {string} registry - Registry URL
87
- * @param {string[]} tags - Tags to push
88
- * @throws {Error} If push fails
89
- */
90
- async function executePush(appName, registry, tags) {
91
- if (await pushUtils.checkACRAuthentication(registry)) {
92
- logger.log(formatSuccessLine(`Already authenticated with ${registry}`));
93
- } else {
94
- await pushUtils.authenticateACR(registry);
95
- }
96
-
97
- await Promise.all(tags.map(async(tag) => {
98
- await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
99
- await pushUtils.pushImage(`${registry}/${appName}:${tag}`, registry);
100
- }));
101
- }
102
-
103
- /**
104
- * Verifies push result
105
- * @function verifyPushResult
106
- * @param {string[]} tags - Tags that were pushed
107
- * @param {string} registry - Registry URL
108
- * @param {string} appName - Application name
109
- */
110
- function verifyPushResult(tags, registry, appName) {
111
- logger.log(formatSuccessParagraph(`Successfully pushed ${tags.length} tag(s) to ${registry}`));
112
- logger.log(chalk.gray(`Image: ${registry}/${appName}:*`));
113
- logger.log(chalk.gray(`Tags: ${tags.join(', ')}`));
114
- }
115
-
116
- /**
117
- * Pushes application image to Azure Container Registry
118
- * @async
119
- * @function pushApp
120
- * @param {string} appName - Name of the application
121
- * @param {Object} options - Push options (registry, tag)
122
- * @returns {Promise<void>} Resolves when push is complete
123
- */
124
- async function pushApp(appName, options = {}) {
125
- try {
126
- validateAppName(appName);
127
-
128
- const { getBuilderPath, resolveApplicationConfigPath } = require('../utils/paths');
129
- const { loadConfigFile } = require('../utils/config-format');
130
- const builderPath = getBuilderPath(appName);
131
- let config;
132
- try {
133
- const configPath = resolveApplicationConfigPath(builderPath);
134
- config = loadConfigFile(configPath);
135
- } catch (error) {
136
- throw new Error(`Failed to load configuration: ${error.message}\nRun 'aifabrix create ${appName}' first`);
137
- }
138
-
139
- const registry = options.registry || config.image?.registry;
140
- if (!registry) {
141
- throw new Error('Registry URL is required. Provide via --registry flag or configure in application.yaml under image.registry');
142
- }
143
-
144
- if (!/^[^.]+\.azurecr\.io$/.test(registry)) {
145
- throw new Error(`Invalid ACR URL format: ${registry}. Expected format: *.azurecr.io`);
146
- }
147
-
148
- const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
149
-
150
- await validatePushPrerequisites(appName, registry);
151
- await executePush(appName, registry, tags);
152
- verifyPushResult(tags, registry, appName);
153
-
154
- } catch (error) {
155
- throw new Error(`Failed to push application: ${error.message}`);
156
- }
157
- }
158
-
159
58
  /**
160
59
  * Generates and validates deployment manifest
161
60
  * @async
@@ -492,7 +391,6 @@ async function deployApp(appName, options = {}) {
492
391
  }
493
392
 
494
393
  module.exports = {
495
- pushApp,
496
394
  deployApp,
497
395
  validateAppName,
498
396
  validateImageIsPullable
package/lib/cli/index.js CHANGED
@@ -41,6 +41,8 @@ const { setupChannelCommands } = require('../commands/channel');
41
41
  const { setupManifestCommands } = require('../commands/manifest');
42
42
  const { setupNpmCommands } = require('./setup-npm');
43
43
  const { setupListCommand } = require('./setup-list');
44
+ const { setupHelpCommand } = require('./setup-help');
45
+ const { setupOnboardingCommand } = require('./setup-onboarding');
44
46
 
45
47
  /**
46
48
  * Dev, secrets, parameters, and platform commands (keeps setupCommands under statement limit).
@@ -101,6 +103,8 @@ function setupExtendedCliCommands(program) {
101
103
  * @param {Command} program - Commander program instance
102
104
  */
103
105
  function setupCommands(program) {
106
+ setupHelpCommand(program);
107
+ setupOnboardingCommand(program);
104
108
  setupCoreCliCommands(program);
105
109
  setupExtendedCliCommands(program);
106
110
  setupDevAndPlatformCommands(program);
@@ -134,6 +134,10 @@ function addPrintHomeWorkCommands(dev) {
134
134
  }
135
135
  });
136
136
 
137
+ addShellEnvCommand(dev);
138
+ }
139
+
140
+ function addShellEnvCommand(dev) {
137
141
  dev
138
142
  .command('shell-env')
139
143
  .description(
@@ -44,8 +44,11 @@ const DEV_ADD_HELP_AFTER = `
44
44
  Examples:
45
45
  $ aifabrix dev add --developer-id 02 --name "Jane Doe" --email jane@example.com
46
46
  $ aifabrix dev add --developer-id 03 --name "Build admin" --email admin@example.com --groups admin,developer
47
- $ aifabrix dev add --developer-id 04 --name "CI user" --email ci@example.com --groups secret-manager,developer
48
- $ aifabrix dev add --developer-id 05 --name "Integrator" --email integrator@example.com --groups developer,secret-reader
47
+ $ aifabrix dev add --developer-id 04 --name "Docker developer" --email docker-dev@example.com --groups developer,docker
48
+ $ aifabrix dev add --developer-id 05 --name "Integrator" --email integrator@example.com --groups integrator
49
+
50
+ Groups: integrator is for SSH onboarding without Docker. Use developer,docker for host Docker access (root-equivalent; grant sparingly).
51
+ When --groups is supplied, it replaces the default developer group; developer is not added automatically.
49
52
 
50
53
  Requires a configured remote Builder Server and an admin client certificate (same machine setup as dev list / dev pin). After add, run dev pin <id> once to create a PIN for aifabrix dev init.
51
54
  `;
@@ -318,7 +321,7 @@ function setupDevListAddCommands(dev) {
318
321
  .requiredOption('--developer-id <id>', 'Unique id, digits only (e.g. 02); used with dev init --developer-id')
319
322
  .requiredOption('--name <name>', 'Display name')
320
323
  .requiredOption('--email <email>', 'Email address')
321
- .option('--groups <items>', 'Comma-separated roles: admin, secret-manager, secret-reader, developer, docker', 'developer')
324
+ .option('--groups <items>', 'Comma-separated roles: admin, secret-manager, secret-reader, developer, docker, integrator', 'developer')
322
325
  .option('--no-provision', 'Create user in database only (skip host provision job)')
323
326
  .option('--builder-version <ver>', 'Builder CLI version for provision job')
324
327
  .option('--admin-email <email>', 'Platform admin email for provision job')
@@ -373,7 +376,7 @@ function setupDevUpdatePinDeleteCommands(dev) {
373
376
  .option('--developer-id <id>', 'Developer ID (same as dev add)')
374
377
  .option('--name <name>', 'Display name')
375
378
  .option('--email <email>', 'Email address')
376
- .option('--groups <items>', 'Comma-separated groups (admin, secret-manager, secret-reader, developer, docker)')
379
+ .option('--groups <items>', 'Comma-separated groups (admin, secret-manager, secret-reader, developer, docker, integrator)')
377
380
  .addHelpText('after', DEV_UPDATE_HELP_AFTER)
378
381
  .action(async(developerId, options) => {
379
382
  try {
@@ -0,0 +1,86 @@
1
+ /** Read-only onboarding guide; command reference remains available through --help. */
2
+ 'use strict';
3
+
4
+ const ONBOARDING_GUIDE = `Welcome to AI Fabrix
5
+
6
+ On a managed workspace host with provisioned config, run:
7
+
8
+ af onboarding
9
+ af onboarding --coding claude
10
+ af onboarding --coding codex
11
+ af onboarding --coding cursor
12
+ af onboarding --coding claude --name sales-workspace
13
+ af onboarding --coding claude --name finance-workspace
14
+ af onboarding --repo example-org/example-workspace
15
+ af onboarding --repo my-org/new-repo
16
+
17
+ This prepares the coding platform selected by your organisation, or the one
18
+ you pass for this run. Claude starts in a detached tmux session; Codex and
19
+ Cursor use their own SSH clients. It does not install the local AI Fabrix platform.
20
+ Use --name <folder> for separate workspaces; each Claude workspace has its own
21
+ tmux session. Use --repo owner/name to clone an accessible GitHub repository
22
+ or create a new private one. agent-kit remote create creates a new GitHub
23
+ repository for the current local workspace; --name sets that remote name.
24
+
25
+ Start your integration workspace with Codex:
26
+
27
+ aifabrix agent-kit setup <controller-url>
28
+
29
+ Ask your administrator for the Controller URL.
30
+ Setup guides you through sign-in, installs the Agent Kit,
31
+ and starts your coding agent.
32
+
33
+ Run setup in a Linux/macOS terminal with Builder, Git and tmux installed.
34
+ On Windows, connect to your organisation's Builder Server first.
35
+ Your files stay under the AI Fabrix work directory on the machine running setup.
36
+ No GitHub account or Docker is required for the default local workspace.
37
+
38
+ Optional:
39
+ Use GitHub (requires gh):
40
+ aifabrix agent-kit setup <controller-url> --repo owner/project
41
+
42
+ Use Claude:
43
+ aifabrix agent-kit setup <controller-url> --code claude
44
+
45
+ Already have a project?
46
+ Open its Git repository folder and install the Agent Kit:
47
+ aifabrix agent-kit install --host codex
48
+
49
+ Using your organisation's Builder Server?
50
+ On your own computer, use the onboarding command your administrator gives you:
51
+ aifabrix dev init --developer-id <id> --server <builder-server-url> --pin <pin>
52
+
53
+ Use the SSH command printed after successful onboarding to connect.
54
+ Once connected to a managed workspace, run af onboarding.
55
+ For a portable server without managed config, use agent-kit setup above.
56
+ Integrator accounts use the integrator group; Docker is not needed.
57
+
58
+ Controller URL: connects your workspace to AI Fabrix.
59
+ Builder Server URL: connects your computer to the remote workspace host.
60
+ These are different services; ask your administrator for the correct URLs.
61
+
62
+ Explore:
63
+ aifabrix --help
64
+ aifabrix agent-kit --help
65
+ aifabrix <command> --help
66
+
67
+ This guide does not sign you in, install software, or change configuration.
68
+ `;
69
+
70
+ function setupHelpCommand(program) {
71
+ program.addHelpCommand(false);
72
+ program.command('help [command]')
73
+ .description('Show the onboarding guide, or help for a command')
74
+ .addHelpText('after', '\nExamples:\n $ aifabrix help\n $ aifabrix help agent-kit\n')
75
+ .action((name, options, command) => {
76
+ if (!name) {
77
+ command.configureOutput().writeOut(ONBOARDING_GUIDE);
78
+ return;
79
+ }
80
+ const target = program.commands.find(item => item.name() === name || item.aliases().includes(name));
81
+ if (!target) command.error(`Unknown command '${name}'. Run aifabrix --help to list commands.`);
82
+ target.outputHelp();
83
+ });
84
+ }
85
+
86
+ module.exports = { setupHelpCommand, ONBOARDING_GUIDE };