@aifabrix/builder 2.55.0 → 2.55.2

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 (70) hide show
  1. package/lib/agent-kit/host-approve.js +48 -6
  2. package/lib/agent-kit/init.js +2 -2
  3. package/lib/agent-kit/remote-prompts.js +46 -13
  4. package/lib/agent-kit/run.js +1 -1
  5. package/lib/agent-kit/start.js +310 -0
  6. package/lib/app/deploy.js +7 -4
  7. package/lib/app/offer-create-from-template.js +184 -0
  8. package/lib/cli/setup-app.js +8 -1
  9. package/lib/cli/setup-auth.js +11 -2
  10. package/lib/commands/agent-kit-help.js +74 -4
  11. package/lib/commands/agent-kit.js +83 -12
  12. package/lib/commands/auth-status-user.js +99 -0
  13. package/lib/commands/auth-status.js +14 -4
  14. package/lib/commands/datasource-load-export-cli.js +7 -2
  15. package/lib/commands/datasource-test-trust-cli.js +5 -1
  16. package/lib/commands/governance-command-action.js +5 -4
  17. package/lib/commands/identity-membership-role-cache.js +6 -1
  18. package/lib/commands/lifecycle-command-action.js +16 -15
  19. package/lib/commands/login-credentials.js +8 -3
  20. package/lib/commands/login-device.js +14 -5
  21. package/lib/commands/login.js +14 -1
  22. package/lib/commands/logout.js +50 -15
  23. package/lib/commands/role-assistant.js +24 -12
  24. package/lib/commands/verify-governance-command-action.js +9 -7
  25. package/lib/commands/verify-operations-command-action.js +9 -8
  26. package/lib/commands/verify-trust-command-action.js +6 -5
  27. package/lib/external-system/download.js +1 -2
  28. package/lib/generator/helpers.js +12 -1
  29. package/lib/integration-definition/apply-static-enterprise.js +5 -2
  30. package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
  31. package/lib/role-assistant/knowledge-sync.js +9 -126
  32. package/lib/role-assistant/test-cases-evaluate.js +12 -1
  33. package/lib/role-assistant/test-cases-evidence.js +44 -5
  34. package/lib/role-assistant/test-cases-lifecycle.js +9 -3
  35. package/lib/role-assistant/test-runner.js +3 -0
  36. package/lib/schema/infra-parameter.schema.json +51 -3
  37. package/lib/schema/infra.parameter.yaml +476 -40
  38. package/lib/schema/type/document-storage.json +1 -1
  39. package/lib/utils/api-required-permissions.js +271 -0
  40. package/lib/utils/api.js +16 -13
  41. package/lib/utils/cli-json-exit.js +41 -0
  42. package/lib/utils/cli-utils.js +23 -5
  43. package/lib/utils/controller-url.js +78 -4
  44. package/lib/utils/deployment-errors.js +15 -0
  45. package/lib/utils/deployment-validation-helpers.js +4 -1
  46. package/lib/utils/device-code.js +31 -10
  47. package/lib/utils/error-formatters/error-parser.js +16 -1
  48. package/lib/utils/error-formatters/permission-errors.js +19 -5
  49. package/lib/utils/keycloak-realm-ready.js +5 -2
  50. package/lib/utils/logger.js +1 -0
  51. package/lib/utils/paths.js +26 -5
  52. package/lib/utils/secrets-helpers.js +2 -2
  53. package/lib/utils/write-stream-and-wait.js +63 -0
  54. package/package.json +20 -6
  55. package/templates/agent-kit/agent-kit.yaml +1 -1
  56. package/templates/agent-kit/instructions/AGENTKIT.md +25 -4
  57. package/templates/agent-kit/instructions/root.AGENTS.md +1 -1
  58. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  59. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +1 -1
  60. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +2 -1
  61. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +1 -0
  62. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +1 -0
  63. package/templates/agent-kit/skills/shared/hosts.md +13 -26
  64. package/templates/agent-kit/skills/shared/login.md +16 -0
  65. package/templates/agent-kit/workspace/README.md +1 -1
  66. package/templates/applications/dataplane/application.yaml +1 -2
  67. package/templates/applications/dataplane/env.template +5 -5
  68. package/templates/applications/miso-controller/application.yaml +21 -2
  69. package/templates/applications/miso-controller/env.template +89 -17
  70. package/templates/applications/miso-controller/rbac.yaml +5 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Offer to materialize a missing app from shipped application templates.
3
+ *
4
+ * @fileoverview Interactive create-from-template before deploy when the app is not on disk
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const logger = require('../utils/logger');
12
+ const { metadata } = require('../utils/cli-test-layout-chalk');
13
+ const { listAvailableTemplates } = require('../validation/template');
14
+ const { findExistingAppType } = require('../utils/paths');
15
+ const { ensureAppFromTemplate } = require('../commands/up-common');
16
+
17
+ /**
18
+ * @param {Object} [options]
19
+ * @returns {boolean}
20
+ */
21
+ function assumeYesCreate(options = {}) {
22
+ return options.yes === true || options.assumeYes === true;
23
+ }
24
+
25
+ /**
26
+ * @param {Object} [deps]
27
+ * @returns {boolean}
28
+ */
29
+ function isInteractiveCreate(deps = {}) {
30
+ if (process.env.CI === 'true' || process.env.CI === '1') {
31
+ return false;
32
+ }
33
+ if (typeof deps.isTty === 'function') {
34
+ return Boolean(deps.isTty());
35
+ }
36
+ return Boolean(process.stdin && process.stdin.isTTY);
37
+ }
38
+
39
+ /**
40
+ * @param {string[]} templates
41
+ * @returns {string}
42
+ */
43
+ function formatTemplateNames(templates) {
44
+ return templates.join(', ');
45
+ }
46
+
47
+ /**
48
+ * @param {string} appName
49
+ * @param {string[]} templates
50
+ * @returns {Error}
51
+ */
52
+ function buildMissingAppError(appName, templates) {
53
+ const listed = templates.length > 0
54
+ ? `Shipped templates: ${formatTemplateNames(templates)}.`
55
+ : 'No shipped application templates were found.';
56
+ const hint = templates.includes(appName)
57
+ ? `Create it with: aifabrix deploy ${appName} --yes`
58
+ : 'Pass a matching template name, or create the app first.';
59
+ return new Error(
60
+ `App '${appName}' was not found under integration/ or builder/. ${listed} ${hint}`
61
+ );
62
+ }
63
+
64
+ /**
65
+ * @param {string} appName
66
+ * @param {Function} promptFn
67
+ * @returns {Promise<boolean>}
68
+ */
69
+ async function confirmCreateMatchingTemplate(appName, promptFn) {
70
+ const ask = promptFn || ((qs) => require('inquirer').prompt(qs));
71
+ const { create } = await ask([{
72
+ type: 'confirm',
73
+ name: 'create',
74
+ message: `App '${appName}' is not available. Create it from the shipped '${appName}' template?`,
75
+ default: true
76
+ }]);
77
+ return Boolean(create);
78
+ }
79
+
80
+ /**
81
+ * @param {string[]} templates
82
+ * @param {Function} promptFn
83
+ * @returns {Promise<string|null>}
84
+ */
85
+ async function pickShippedTemplate(templates, promptFn) {
86
+ const ask = promptFn || ((qs) => require('inquirer').prompt(qs));
87
+ const { template } = await ask([{
88
+ type: 'list',
89
+ name: 'template',
90
+ message: 'App was not found. Create from which shipped application template?',
91
+ choices: [
92
+ ...templates.map((name) => ({ name, value: name })),
93
+ { name: 'Cancel', value: null }
94
+ ]
95
+ }]);
96
+ return template || null;
97
+ }
98
+
99
+ /**
100
+ * @param {string} appName
101
+ * @param {Object} [deps]
102
+ * @returns {Promise<string>}
103
+ */
104
+ async function materializeShippedTemplate(appName, deps = {}) {
105
+ const copy = deps.ensureAppFromTemplate || ensureAppFromTemplate;
106
+ await copy(appName);
107
+ return appName;
108
+ }
109
+
110
+ /**
111
+ * @param {string} appName
112
+ * @param {string[]} templates
113
+ * @param {Object} options
114
+ * @param {Object} deps
115
+ * @returns {Promise<string>}
116
+ */
117
+ async function createMatchingTemplateApp(appName, templates, options, deps) {
118
+ if (assumeYesCreate(options)) {
119
+ return materializeShippedTemplate(appName, deps);
120
+ }
121
+ if (!isInteractiveCreate(deps)) {
122
+ throw buildMissingAppError(appName, templates);
123
+ }
124
+ const ok = await confirmCreateMatchingTemplate(appName, deps.prompt);
125
+ if (!ok) {
126
+ throw buildMissingAppError(appName, templates);
127
+ }
128
+ return materializeShippedTemplate(appName, deps);
129
+ }
130
+
131
+ /**
132
+ * @param {string} appName
133
+ * @param {string[]} templates
134
+ * @param {Object} options
135
+ * @param {Object} deps
136
+ * @returns {Promise<string>}
137
+ */
138
+ async function createPickedTemplateApp(appName, templates, options, deps) {
139
+ if (assumeYesCreate(options) || !isInteractiveCreate(deps) || templates.length === 0) {
140
+ throw buildMissingAppError(appName, templates);
141
+ }
142
+ const chosen = await pickShippedTemplate(templates, deps.prompt);
143
+ if (!chosen) {
144
+ throw buildMissingAppError(appName, templates);
145
+ }
146
+ if (chosen !== appName) {
147
+ logger.log(metadata(
148
+ `Creating '${chosen}' from shipped template (requested '${appName}' was not found).`
149
+ ));
150
+ }
151
+ await materializeShippedTemplate(chosen, deps);
152
+ return chosen;
153
+ }
154
+
155
+ /**
156
+ * If the app is missing locally, offer to create it from shipped templates.
157
+ *
158
+ * @async
159
+ * @function offerCreateFromTemplateIfMissing
160
+ * @param {string} appName - Requested app or system key
161
+ * @param {Object} [options] - CLI options (`yes` skips the confirm when the name matches a template)
162
+ * @param {Object} [deps] - Test seams (prompt, isTty, listTemplates, findExisting, ensureAppFromTemplate)
163
+ * @returns {Promise<string>} App key to continue with (may differ if the user picked another template)
164
+ * @throws {Error} When the app is missing and the user declines or cannot be prompted
165
+ */
166
+ async function offerCreateFromTemplateIfMissing(appName, options = {}, deps = {}) {
167
+ const findExisting = deps.findExisting || findExistingAppType;
168
+ if (findExisting(appName, options)) {
169
+ return appName;
170
+ }
171
+ const listTemplates = deps.listTemplates || listAvailableTemplates;
172
+ const templates = await listTemplates();
173
+ if (templates.includes(appName)) {
174
+ return createMatchingTemplateApp(appName, templates, options, deps);
175
+ }
176
+ return createPickedTemplateApp(appName, templates, options, deps);
177
+ }
178
+
179
+ module.exports = {
180
+ offerCreateFromTemplateIfMissing,
181
+ buildMissingAppError,
182
+ assumeYesCreate,
183
+ isInteractiveCreate
184
+ };
@@ -39,6 +39,10 @@ Examples:
39
39
  $ aifabrix deploy myapp
40
40
  $ aifabrix deploy myapp --local
41
41
 
42
+ # Missing local app that matches a shipped template (TTY asks to create)
43
+ $ aifabrix deploy dataplane
44
+ $ aifabrix deploy dataplane --yes
45
+
42
46
  # Local — external system (integration/<systemKey>/); manifest regenerated on deploy
43
47
  $ aifabrix deploy hubspot-demo
44
48
  $ aifabrix deploy hubspot-demo --local
@@ -69,6 +73,7 @@ Notes:
69
73
  - Default output is a compact status card; use -v for expanded preflight and readiness detail.
70
74
  - Controller deploy always polls to a terminal status (no --poll / --no-poll on this command).
71
75
  - --repository-url overrides repo URL for pipeline validate (default: application.yaml repository.repositoryUrl).
76
+ - If the app is missing locally and the name matches a shipped template, TTY asks to create it (use --yes in scripts).
72
77
  `;
73
78
 
74
79
  const PUSH_HELP_AFTER = `
@@ -367,6 +372,7 @@ function setupDeployCommand(program) {
367
372
  '--subject-email <emails>',
368
373
  'Subject email(s) for governance gate (comma-separated; used with --gate governance|full)'
369
374
  )
375
+ .option('-y, --yes', 'Create a missing app from a matching shipped template without prompting')
370
376
  .action(async(appName, options) => {
371
377
  try {
372
378
  const probeTimeout =
@@ -381,8 +387,9 @@ function setupDeployCommand(program) {
381
387
  };
382
388
  const outcome = await app.deployApp(appName, opts);
383
389
  if (opts.local && outcome) {
390
+ const targetApp = outcome.appKey || appName;
384
391
  if (outcome.usedExternalDeploy) await app.restartApp('dataplane');
385
- else await app.runApp(appName, opts);
392
+ else await app.runApp(targetApp, opts);
386
393
  }
387
394
  } catch (error) {
388
395
  handleCommandError(error, 'deploy');
@@ -17,7 +17,12 @@ const { handleAuthConfig } = require('../commands/auth-config');
17
17
  const LOGIN_HELP_AFTER = `
18
18
  Examples:
19
19
  $ aifabrix login
20
+ $ aifabrix login -c https://controller.example.com
20
21
  $ aifabrix login -m credentials -a myapp -e dev
22
+
23
+ Notes:
24
+ - Public controller URLs are stored as origin + /miso (extra path segments are ignored).
25
+ - Localhost URLs keep the origin unless the path is already /miso.
21
26
  `;
22
27
 
23
28
  const AUTH_HELP_AFTER = `
@@ -34,12 +39,15 @@ Subcommand: auth status [--validate] for CI/scripts (exit codes 0/1/3).
34
39
  const LOGOUT_HELP_AFTER = `
35
40
  Examples:
36
41
  $ aifabrix logout
42
+ $ aifabrix logout --type client
43
+ $ aifabrix logout --type device
37
44
  $ aifabrix logout -c http://localhost:3600
38
45
  $ aifabrix logout -e dev
39
46
  $ aifabrix logout -e dev -a hubspot-e2e
40
47
 
41
48
  Notes:
42
49
  - With no flags: clears all device tokens and all client tokens in config.yaml.
50
+ - Use --type client or --type device to clear only that token type.
43
51
  - Device tokens are keyed by controller URL (-c).
44
52
  - Client tokens are per environment and app (-e and optional -a).
45
53
  - Run aifabrix auth status to list stored app tokens and copy-paste logout commands.
@@ -164,7 +172,7 @@ function setupLoginCommand(program) {
164
172
  program.command('login')
165
173
  .description('Sign in to Miso Controller (device or credentials flow)')
166
174
  .addHelpText('after', LOGIN_HELP_AFTER)
167
- .option('-c, --controller <url>', 'Controller URL (default: from config or developer ID, e.g. http://localhost:3000)')
175
+ .option('-c, --controller <url>', 'Controller URL (public hosts use origin + /miso; default: config or localhost by developer ID)')
168
176
  .option('-m, --method <method>', 'Authentication method (device|credentials)', 'device')
169
177
  .option('-a, --app <app>', 'Application name (required for credentials method, reads from secrets.local.yaml)')
170
178
  .option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
@@ -188,7 +196,8 @@ function setupLoginCommand(program) {
188
196
 
189
197
  function setupLogoutCommand(program) {
190
198
  program.command('logout')
191
- .description('Clear stored tokens (optional filter by controller/env/app)')
199
+ .description('Clear stored tokens (optional filter by type/controller/env/app)')
200
+ .option('-t, --type <type>', 'Clear only one token type (device|client)')
192
201
  .option('-c, --controller <url>', 'Clear device tokens for specific controller')
193
202
  .option('-e, --environment <env>', 'Clear client tokens for specific environment')
194
203
  .option('-a, --app <app>', 'Clear client tokens for specific app (requires --environment)')
@@ -5,24 +5,46 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const HOST_OPTION_HELP =
9
+ 'Coding hosts to activate: all, cursor, claude, or codex (one name = that host only)';
10
+
11
+ const APPROVE_ALL_OPTION_HELP =
12
+ 'Auto-approve tools for all, claude, or codex (omit value = every selected host)';
13
+
8
14
  const GROUP_HELP = `
15
+ Coding hosts (--host) are Cursor, Claude Code, and/or Codex in this workspace.
16
+ GitHub remotes are a separate command (agent-kit remote create).
17
+
9
18
  Examples:
10
19
  $ aifabrix agent-kit init cust-demo --host all --approve-all --yes
11
- $ aifabrix agent-kit install --host all --approve-all --yes
20
+ $ aifabrix agent-kit install --host claude --approve-all --yes
21
+ $ aifabrix agent-kit install --host all --approve-all claude --yes
12
22
  $ aifabrix agent-kit update --host all --approve-all --yes
23
+ $ aifabrix agent-kit remote create --org my-org --yes
24
+ $ aifabrix agent-kit start claude --remote-control --approve-all --yes
25
+ $ aifabrix agent-kit start claude --name aifabrix-docs --remote-control --approve-all --yes
26
+ $ aifabrix agent-kit start codex --approve-all --yes
13
27
  $ aifabrix agent-kit status
14
28
  $ aifabrix agent-kit validate
15
29
  `;
16
30
 
17
31
  const INSTALL_HELP = `
32
+ --host chooses which coding apps this repo supports (default all).
33
+ Use one name to activate only that host. --approve-all [claude|codex] writes
34
+ auto-approve configs for that host's tools only.
35
+
18
36
  Examples:
19
37
  $ aifabrix agent-kit install
20
38
  $ aifabrix agent-kit install --host all --approve-all --yes
21
39
  $ aifabrix agent-kit install --host claude --approve-all --yes
40
+ $ aifabrix agent-kit install --host codex --approve-all --yes
41
+ $ aifabrix agent-kit install --host all --approve-all claude --yes
22
42
  $ aifabrix agent-kit install --skip-approve-all
23
43
  $ aifabrix agent-kit install --dry-run
24
44
 
25
- TTY asks to auto-approve Claude Code and Codex permissions (default yes). Silent: --approve-all --yes. Skip: --skip-approve-all.
45
+ TTY asks which hosts to support (omit --host) and which tools to auto-approve.
46
+ Silent: --host <name> --approve-all [claude|codex] --yes. Skip: --skip-approve-all.
47
+ Start without installing skills: aifabrix agent-kit start claude
26
48
  `;
27
49
 
28
50
  const STATUS_HELP = `
@@ -36,9 +58,12 @@ Examples:
36
58
  $ aifabrix agent-kit update
37
59
  $ aifabrix agent-kit update --host all --approve-all --yes
38
60
  $ aifabrix agent-kit update --host claude --force
61
+ $ aifabrix agent-kit update --host all --approve-all claude --yes
39
62
  $ aifabrix agent-kit update --dry-run
40
63
 
41
- Omit --host to keep the coding hosts already recorded. Pass --host when you add or switch Cursor, Claude Code, or Codex.
64
+ Omit --host to keep the coding hosts already recorded. Pass --host claude or
65
+ --host codex to activate only that host. --approve-all claude writes Claude
66
+ Code tool auto-approve only.
42
67
  `;
43
68
 
44
69
  const VALIDATE_HELP = `
@@ -48,18 +73,39 @@ Examples:
48
73
  `;
49
74
 
50
75
  const INIT_HELP = `
76
+ --host chooses Cursor, Claude Code, and/or Codex for the new workspace.
77
+ GitHub (origin + push) is --remote / agent-kit remote create — not --host.
78
+
51
79
  Examples:
52
80
  $ aifabrix agent-kit init cust-demo
53
81
  $ aifabrix agent-kit init cust-demo --host all --approve-all --yes
82
+ $ aifabrix agent-kit init cust-demo --host claude --approve-all --yes
83
+ $ aifabrix agent-kit init cust-demo --host all --approve-all claude --yes
54
84
  $ aifabrix agent-kit init cust-demo --host all --remote --org my-org --yes
55
85
  $ aifabrix agent-kit init ./path/to/cust-demo --dry-run --json
56
86
 
57
87
  Interactive (TTY): omit --host / --yes to be prompted.
58
88
  Silent local: --host all --approve-all --yes
89
+ Silent one host: --host claude --approve-all --yes
59
90
  Silent remote: --host all --remote --org <owner> --yes
60
91
  `;
61
92
 
93
+ const REMOTE_GROUP_HELP = `
94
+ This is GitHub only: create origin and push this workspace.
95
+ It does not choose Cursor, Claude Code, or Codex.
96
+
97
+ Examples:
98
+ $ aifabrix agent-kit remote create
99
+ $ aifabrix agent-kit remote create --org my-org --yes
100
+ $ aifabrix agent-kit install --host claude --approve-all --yes
101
+ $ aifabrix agent-kit install --host codex --approve-all --yes
102
+ $ aifabrix agent-kit install --host all --approve-all claude --yes
103
+ `;
104
+
62
105
  const REMOTE_HELP = `
106
+ Creates a GitHub repository (origin) and pushes this workspace.
107
+ Not coding-host selection — use --host on install/update for Claude or Codex.
108
+
63
109
  Examples:
64
110
  $ cd cust-demo && aifabrix agent-kit remote create
65
111
  $ aifabrix agent-kit remote create --org my-org --yes
@@ -68,12 +114,36 @@ Examples:
68
114
  Interactive (TTY): omit flags to be prompted. Silent: --org <owner> --yes
69
115
  `;
70
116
 
117
+ const START_HELP = `
118
+ Does not install Agent Kit skills. Starts claude or codex in tmux in this git
119
+ workspace so closing the terminal does not kill the session. Detach: Ctrl-b then
120
+ d. --approve-all writes that app's local permission config only. --name is the
121
+ tmux session (default: last folder, e.g. aifabrix-docs). Claude also gets
122
+ --name and --remote-control hostname-user-folder, e.g.
123
+ builder01-dev03-aifabrix-docs. Claude --tmux is for worktrees, not this wrapper.
124
+ That is not Claude --cloud (a separate cloud VM). Codex has no phone/web attach.
125
+
126
+ Examples:
127
+ $ aifabrix agent-kit start claude --remote-control --approve-all --yes
128
+ $ aifabrix agent-kit start claude --name aifabrix-docs --remote-control --approve-all --yes
129
+ $ aifabrix agent-kit start claude --name aifabrix-miso --remote-control --approve-all --yes
130
+ $ aifabrix agent-kit start claude --remote-control --detach
131
+ $ aifabrix agent-kit start claude
132
+ $ aifabrix agent-kit start claude --approve-all --yes
133
+ $ aifabrix agent-kit start codex --approve-all --yes
134
+ $ aifabrix agent-kit start claude --dry-run --json
135
+ `;
136
+
71
137
  module.exports = {
138
+ HOST_OPTION_HELP,
139
+ APPROVE_ALL_OPTION_HELP,
72
140
  GROUP_HELP,
73
141
  INSTALL_HELP,
74
142
  STATUS_HELP,
75
143
  UPDATE_HELP,
76
144
  VALIDATE_HELP,
77
145
  INIT_HELP,
78
- REMOTE_HELP
146
+ REMOTE_GROUP_HELP,
147
+ REMOTE_HELP,
148
+ START_HELP
79
149
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `aifabrix agent-kit` command group.
3
- * @fileoverview Install, init, status, update, validate, remote create
3
+ * @fileoverview Install, init, status, update, validate, remote create, start
4
4
  */
5
5
 
6
6
  'use strict';
@@ -9,6 +9,7 @@ const logger = require('../utils/logger');
9
9
  const { handleCommandError } = require('../utils/cli-utils');
10
10
  const { formatSuccessLine, formatBlockingError, formatWarningLine } = require('../utils/cli-layout-chalk');
11
11
  const { runInstall, runUpdate } = require('../agent-kit/run');
12
+ const { runStart } = require('../agent-kit/start');
12
13
  const path = require('path');
13
14
  const { runInit, runRemoteCreate } = require('../agent-kit/init');
14
15
  const { enrichInitHostOptions, enrichInitRemoteOptions, enrichRemoteOptions, enrichApproveAllOptions } = require('../agent-kit/remote-prompts');
@@ -26,7 +27,11 @@ const {
26
27
  UPDATE_HELP,
27
28
  VALIDATE_HELP,
28
29
  INIT_HELP,
29
- REMOTE_HELP
30
+ REMOTE_GROUP_HELP,
31
+ REMOTE_HELP,
32
+ START_HELP,
33
+ HOST_OPTION_HELP,
34
+ APPROVE_ALL_OPTION_HELP
30
35
  } = require('./agent-kit-help');
31
36
 
32
37
  /**
@@ -91,6 +96,34 @@ function emitRemoteReport(json, asJson) {
91
96
  logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Done'));
92
97
  }
93
98
 
99
+ /**
100
+ * @param {object} json
101
+ * @param {boolean} asJson
102
+ * @returns {void}
103
+ */
104
+ function emitStartReport(json, asJson) {
105
+ if (asJson) {
106
+ logger.log(JSON.stringify(json, null, 2));
107
+ return;
108
+ }
109
+ logger.log(`App ${json.app}`);
110
+ logger.log(`Session ${json.session}`);
111
+ if (json.remoteControlName) {
112
+ logger.log(`Remote ${json.remoteControlName}`);
113
+ }
114
+ logger.log(`Path ${json.path}`);
115
+ logger.log(`Launch ${json.launch.join(' ')}`);
116
+ if (json.executed && json.detach) {
117
+ logger.log(formatSuccessLine('Session started in background'));
118
+ return;
119
+ }
120
+ if (json.executed) {
121
+ logger.log(formatSuccessLine('Session ended'));
122
+ return;
123
+ }
124
+ logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Ready'));
125
+ }
126
+
94
127
  /**
95
128
  * @param {Function} runner
96
129
  * @param {object} options
@@ -125,6 +158,7 @@ function setupAgentKitCommands(program) {
125
158
  addUpdateCommand(group);
126
159
  addValidateCommand(group);
127
160
  addRemoteCommand(group);
161
+ addStartCommand(group);
128
162
  }
129
163
 
130
164
  /**
@@ -134,16 +168,16 @@ function addInitCommand(group) {
134
168
  group
135
169
  .command('init [nameOrPath]')
136
170
  .description('Create a customer workspace skeleton, git repo, and Agent Kit')
137
- .option('--host <target>', 'all, cursor, codex, or claude (omit on TTY to choose)', 'all')
171
+ .option('--host <target>', HOST_OPTION_HELP, 'all')
138
172
  .option('--scm <kind>', 'git or none', 'git')
139
- .option('--remote', 'Create private hosting remote and push (requires auth)')
173
+ .option('--remote', 'Create private GitHub remote and push (requires auth)')
140
174
  .option('--no-remote', 'Force local-only (skip remote prompt on TTY)')
141
175
  .option('--org <owner>', 'Hosting org or user (required with --remote when non-TTY)')
142
176
  .option('--visibility <mode>', 'private or public', 'private')
143
177
  .option('--license <kind>', 'mit or none', 'none')
144
178
  .option('--force', 'Allow non-empty target directory')
145
179
  .option('--yes', 'Silent: skip TTY confirm (pair with --host and --org); also auto-approves host permissions')
146
- .option('--approve-all', 'Write Claude Code and Codex auto-approve configs')
180
+ .option('--approve-all [host]', APPROVE_ALL_OPTION_HELP)
147
181
  .option('--skip-approve-all', 'Do not write Claude Code or Codex auto-approve configs')
148
182
  .option('--dry-run', 'Plan writes without changing files')
149
183
  .option('--json', 'Machine-readable output')
@@ -177,10 +211,13 @@ function addInitCommand(group) {
177
211
  * @param {import('commander').Command} group
178
212
  */
179
213
  function addRemoteCommand(group) {
180
- const remote = group.command('remote').description('Hosting remotes for an Agent Kit workspace');
214
+ const remote = group
215
+ .command('remote')
216
+ .description('Create a GitHub repository for this workspace and push')
217
+ .addHelpText('after', REMOTE_GROUP_HELP);
181
218
  remote
182
219
  .command('create')
183
- .description('Create a private GitHub remote and push the current workspace')
220
+ .description('Create a private GitHub repository (origin) and push this workspace')
184
221
  .option('--org <owner>', 'Hosting org or user (required when non-TTY)')
185
222
  .option('--name <repo>', 'Remote repository name (default: directory basename)')
186
223
  .option('--visibility <mode>', 'private or public', 'private')
@@ -215,8 +252,8 @@ function addInstallCommand(group) {
215
252
  group
216
253
  .command('install')
217
254
  .description('Install the Agent Kit into the current git repository')
218
- .option('--host <target>', 'all, cursor, codex, or claude', 'all')
219
- .option('--approve-all', 'Write Claude Code and Codex auto-approve configs')
255
+ .option('--host <target>', HOST_OPTION_HELP, 'all')
256
+ .option('--approve-all [host]', APPROVE_ALL_OPTION_HELP)
220
257
  .option('--skip-approve-all', 'Do not write Claude Code or Codex auto-approve configs')
221
258
  .option('--yes', 'Silent: auto-approve host permission configs without asking')
222
259
  .option('--dry-run', 'Plan writes without changing files')
@@ -225,7 +262,8 @@ function addInstallCommand(group) {
225
262
  .action(async(options) => {
226
263
  try {
227
264
  const interactive = Boolean(process.stdout.isTTY) && !options.json;
228
- const merged = await enrichApproveAllOptions(options, { interactive });
265
+ const withHost = await enrichInitHostOptions(options, { interactive });
266
+ const merged = await enrichApproveAllOptions(withHost, { interactive });
229
267
  await runLeaf(runInstall, merged, true);
230
268
  } catch (error) {
231
269
  handleCommandError(error, 'agent-kit install');
@@ -287,8 +325,8 @@ function addUpdateCommand(group) {
287
325
  group
288
326
  .command('update')
289
327
  .description('Update managed Agent Kit files from this Builder release')
290
- .option('--host <target>', 'all, cursor, codex, or claude (omit to keep recorded hosts)')
291
- .option('--approve-all', 'Write Claude Code and Codex auto-approve configs')
328
+ .option('--host <target>', `${HOST_OPTION_HELP} (omit to keep recorded hosts)`)
329
+ .option('--approve-all [host]', APPROVE_ALL_OPTION_HELP)
292
330
  .option('--skip-approve-all', 'Do not write Claude Code or Codex auto-approve configs')
293
331
  .option('--yes', 'Silent: auto-approve host permission configs without asking')
294
332
  .option('--dry-run', 'Plan writes without changing files')
@@ -343,4 +381,37 @@ async function runValidateLeaf(options) {
343
381
  }
344
382
  }
345
383
 
384
+ /**
385
+ * @param {import('commander').Command} group
386
+ */
387
+ function addStartCommand(group) {
388
+ group
389
+ .command('start <app>')
390
+ .description('Start Claude Code or Codex in tmux (does not install skills)')
391
+ .option('--approve-all', 'Write auto-approve config for this app only')
392
+ .option('--skip-approve-all', 'Do not write auto-approve configs')
393
+ .option('--yes', 'Same as --approve-all')
394
+ .option('--detach', 'Create the tmux session in the background (no attach)')
395
+ .option(
396
+ '--name <session>',
397
+ 'tmux session name (default: last folder of this git workspace)'
398
+ )
399
+ .option(
400
+ '--remote-control',
401
+ 'Claude Code only: same session on this PC, claude.ai/code, and the Claude mobile app'
402
+ )
403
+ .option('--dry-run', 'Print the launch command without starting it')
404
+ .option('--json', 'Machine-readable output')
405
+ .addHelpText('after', START_HELP)
406
+ .action(async(app, options) => {
407
+ try {
408
+ const json = runStart({ ...options, app });
409
+ emitStartReport(json, Boolean(options.json));
410
+ } catch (error) {
411
+ handleCommandError(error, 'agent-kit start');
412
+ process.exitCode = 1;
413
+ }
414
+ });
415
+ }
416
+
346
417
  module.exports = { setupAgentKitCommands };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Resolve the signed-in username for `aifabrix auth status`.
3
+ *
4
+ * @fileoverview Unwrap controller auth/user payloads and pick a display username
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { unwrapControllerData } = require('../identity/identity-response-helpers');
12
+
13
+ /**
14
+ * @param {...*} values
15
+ * @returns {string}
16
+ */
17
+ function firstNonEmptyString(...values) {
18
+ for (const value of values) {
19
+ if (typeof value === 'string' && value.trim()) {
20
+ return value.trim();
21
+ }
22
+ }
23
+ return '';
24
+ }
25
+
26
+ /**
27
+ * Decode a JWT payload for display only. Does not verify the signature.
28
+ * @param {string} token
29
+ * @returns {Object|null}
30
+ */
31
+ function decodeJwtPayload(token) {
32
+ if (!token || typeof token !== 'string') {
33
+ return null;
34
+ }
35
+ const parts = token.split('.');
36
+ if (parts.length < 2) {
37
+ return null;
38
+ }
39
+ try {
40
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
41
+ const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, '=');
42
+ const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
43
+ return payload && typeof payload === 'object' ? payload : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {Object|null|undefined} user
51
+ * @param {string} [token]
52
+ * @returns {string}
53
+ */
54
+ function resolveDisplayUsername(user, token) {
55
+ const fromUser = firstNonEmptyString(
56
+ user && user.username,
57
+ user && user.preferredUsername,
58
+ user && user.preferred_username,
59
+ user && user.email
60
+ );
61
+ if (fromUser) {
62
+ return fromUser;
63
+ }
64
+ const payload = decodeJwtPayload(token);
65
+ return firstNonEmptyString(
66
+ payload && payload.preferred_username,
67
+ payload && payload.preferredUsername,
68
+ payload && payload.email,
69
+ payload && payload.upn
70
+ );
71
+ }
72
+
73
+ /**
74
+ * @param {Object|null|undefined} response
75
+ * @param {string} [token]
76
+ * @returns {{ authenticated: boolean, user: Object|null, username: string }}
77
+ */
78
+ function parseAuthUserResponse(response, token) {
79
+ const payload = unwrapControllerData(response);
80
+ if (!payload || typeof payload !== 'object') {
81
+ return { authenticated: false, user: null, username: '' };
82
+ }
83
+ const rawUser = payload.user && typeof payload.user === 'object' ? payload.user : null;
84
+ const username = resolveDisplayUsername(rawUser, token);
85
+ const user = username && (!rawUser || !rawUser.username)
86
+ ? { ...(rawUser || {}), username }
87
+ : rawUser;
88
+ return {
89
+ authenticated: payload.authenticated !== false,
90
+ user,
91
+ username
92
+ };
93
+ }
94
+
95
+ module.exports = {
96
+ decodeJwtPayload,
97
+ resolveDisplayUsername,
98
+ parseAuthUserResponse
99
+ };