@msn-control/liftoff 0.4.0 → 0.5.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 (123) hide show
  1. package/README.md +65 -224
  2. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +21 -0
  3. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +520 -0
  4. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +27 -0
  5. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +25 -0
  6. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +22 -0
  7. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +23 -0
  8. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +13 -0
  9. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +6597 -0
  10. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +60 -0
  11. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +55 -0
  12. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +17 -0
  13. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +1 -0
  14. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +37 -0
  15. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +46 -0
  16. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +60 -0
  17. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +211 -0
  18. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +92 -0
  19. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +355 -0
  20. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +32 -0
  21. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +184 -0
  22. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +141 -0
  23. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +257 -0
  24. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +21 -0
  25. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +22 -0
  26. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +46 -0
  27. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +29 -0
  28. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +185 -0
  29. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +26 -0
  30. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +13 -0
  31. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +114 -0
  32. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +66 -0
  33. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +18 -0
  34. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +61 -0
  35. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +11 -0
  36. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +131 -0
  37. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +6 -0
  38. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +10 -0
  39. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +31 -0
  40. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +59 -0
  41. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +14 -0
  42. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +26 -0
  43. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +23 -0
  44. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +64 -0
  45. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +24 -0
  46. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +34 -0
  47. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +13 -0
  48. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +26 -0
  49. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +19 -0
  50. package/assets/power-apps-code-app/README.md +27 -0
  51. package/dist/args.d.ts +33 -0
  52. package/dist/args.js +100 -38
  53. package/dist/args.js.map +1 -1
  54. package/dist/catalogs.d.ts +3 -1
  55. package/dist/catalogs.js +23 -0
  56. package/dist/catalogs.js.map +1 -1
  57. package/dist/cli.js +6 -1
  58. package/dist/cli.js.map +1 -1
  59. package/dist/code-apps-plugin.d.ts +13 -0
  60. package/dist/code-apps-plugin.js +69 -0
  61. package/dist/code-apps-plugin.js.map +1 -0
  62. package/dist/commands.d.ts +4 -0
  63. package/dist/commands.js +669 -321
  64. package/dist/commands.js.map +1 -1
  65. package/dist/file-system.d.ts +16 -1
  66. package/dist/file-system.js +320 -10
  67. package/dist/file-system.js.map +1 -1
  68. package/dist/framework-adapters.d.ts +5 -2
  69. package/dist/framework-adapters.js +5 -2
  70. package/dist/framework-adapters.js.map +1 -1
  71. package/dist/genai-templates.d.ts +6 -6
  72. package/dist/genai-templates.js +0 -3
  73. package/dist/genai-templates.js.map +1 -1
  74. package/dist/interactive.d.ts +80 -6
  75. package/dist/interactive.js +338 -142
  76. package/dist/interactive.js.map +1 -1
  77. package/dist/migrate-plan.d.ts +4 -4
  78. package/dist/migrate-plan.js +1 -1
  79. package/dist/migrate-plan.js.map +1 -1
  80. package/dist/planner.d.ts +5 -0
  81. package/dist/planner.js +197 -53
  82. package/dist/planner.js.map +1 -1
  83. package/dist/power-apps-assets.d.ts +30 -0
  84. package/dist/power-apps-assets.js +131 -0
  85. package/dist/power-apps-assets.js.map +1 -0
  86. package/dist/power-apps-templates.d.ts +3 -0
  87. package/dist/power-apps-templates.js +140 -0
  88. package/dist/power-apps-templates.js.map +1 -0
  89. package/dist/power-apps-validation.d.ts +1 -0
  90. package/dist/power-apps-validation.js +79 -0
  91. package/dist/power-apps-validation.js.map +1 -0
  92. package/dist/process-runner.js +9 -6
  93. package/dist/process-runner.js.map +1 -1
  94. package/dist/project-dependencies.d.ts +7 -4
  95. package/dist/project-dependencies.js +16 -4
  96. package/dist/project-dependencies.js.map +1 -1
  97. package/dist/published-verifier.js +1 -1
  98. package/dist/published-verifier.js.map +1 -1
  99. package/dist/standard-templates.d.ts +4 -4
  100. package/dist/standard-templates.js.map +1 -1
  101. package/dist/templates.d.ts +2 -2
  102. package/dist/templates.js +196 -50
  103. package/dist/templates.js.map +1 -1
  104. package/dist/terminal.d.ts +99 -3
  105. package/dist/terminal.js +482 -66
  106. package/dist/terminal.js.map +1 -1
  107. package/dist/types.d.ts +73 -15
  108. package/dist/workstation.d.ts +10 -5
  109. package/dist/workstation.js +25 -10
  110. package/dist/workstation.js.map +1 -1
  111. package/docs/assets/liftoff-terminal.svg +33 -0
  112. package/docs/azure-deployment.md +66 -0
  113. package/docs/cli-reference.md +128 -0
  114. package/docs/configuration-and-manifests.md +102 -0
  115. package/docs/existing-repositories.md +87 -0
  116. package/docs/getting-started.md +114 -0
  117. package/docs/prerequisites.md +99 -0
  118. package/docs/project-structure.md +119 -0
  119. package/docs/safety-and-consent.md +98 -0
  120. package/docs/spec-workflows-and-agents.md +92 -0
  121. package/docs/troubleshooting.md +133 -0
  122. package/docs/workloads.md +128 -0
  123. package/package.json +8 -2
@@ -1,8 +1,82 @@
1
- import type { ProjectOptions, ProjectPlan } from './types.js';
1
+ import type { Readable } from 'node:stream';
2
2
  import type { DependencyCommandPlan } from './project-dependencies.js';
3
- export declare function promptForInitOptions(initial: ProjectOptions): Promise<ProjectOptions>;
4
- export declare function confirmPlan(plan: ProjectPlan, yes?: boolean): Promise<boolean>;
5
- export declare function confirmToolInstallation(detail: string): Promise<boolean>;
6
- export declare function confirmFileReplacements(paths: readonly string[]): Promise<boolean>;
7
- export declare function confirmDependencyInstallation(commands: DependencyCommandPlan[]): Promise<boolean>;
3
+ import { type CommandRunner } from './process-runner.js';
4
+ import { PresentationSession } from './terminal.js';
5
+ import type { CodingAgentId, ProjectOptions, ProjectPlan } from './types.js';
6
+ interface AgentCheckboxChoice {
7
+ name: string;
8
+ value: CodingAgentId;
9
+ checked: boolean;
10
+ }
11
+ export type AgentCheckboxPrompt = (config: {
12
+ message: string;
13
+ choices: AgentCheckboxChoice[];
14
+ required: boolean;
15
+ validate: (selected: readonly CodingAgentId[]) => true | string;
16
+ }, context: {
17
+ input: Readable;
18
+ output: NodeJS.WritableStream;
19
+ }) => Promise<CodingAgentId[]>;
20
+ export interface InteractiveDependencies {
21
+ input?: Readable;
22
+ output?: NodeJS.WritableStream;
23
+ presentation?: PresentationSession;
24
+ cwd?: string;
25
+ configuredRoot?: string;
26
+ runner?: CommandRunner;
27
+ checkboxPrompt?: AgentCheckboxPrompt;
28
+ }
29
+ export interface ToolInstallationPrompt {
30
+ label: string;
31
+ severity: string;
32
+ purpose: string;
33
+ requirement: string;
34
+ observed: string;
35
+ command?: string;
36
+ remedy?: string;
37
+ }
38
+ export declare class InteractiveCancelledError extends Error {
39
+ constructor(message?: string);
40
+ }
41
+ export declare class InteractivePrompter {
42
+ private readonly input;
43
+ private readonly output;
44
+ private readonly presentation;
45
+ private readonly cwd;
46
+ private readonly configuredRoot?;
47
+ private readonly runner?;
48
+ private readonly checkboxPrompt?;
49
+ private rl?;
50
+ private lines?;
51
+ private closed;
52
+ constructor(dependencies?: InteractiveDependencies);
53
+ close(): void;
54
+ promptForInitOptions(initial: ProjectOptions): Promise<ProjectOptions>;
55
+ confirmPlan(plan: ProjectPlan, yes?: boolean, actionLabel?: string): Promise<boolean>;
56
+ confirmToolInstallation(prompt: ToolInstallationPrompt | string): Promise<boolean>;
57
+ confirmFileReplacements(paths: readonly string[]): Promise<boolean>;
58
+ confirmDependencyInstallation(commands: DependencyCommandPlan[]): Promise<boolean>;
59
+ private askRequired;
60
+ private choose;
61
+ private confirm;
62
+ private promptForRegion;
63
+ private chooseRegion;
64
+ private askEnvironments;
65
+ private askAgents;
66
+ private askAgentsWithLines;
67
+ private askAgentsWithCheckbox;
68
+ private discoverAgents;
69
+ private pathExists;
70
+ private agentChoiceLabel;
71
+ private isRealTty;
72
+ private question;
73
+ private lineIterator;
74
+ private releaseLineInput;
75
+ }
76
+ export declare function promptForInitOptions(initial: ProjectOptions, dependencies?: InteractiveDependencies): Promise<ProjectOptions>;
77
+ export declare function confirmPlan(plan: ProjectPlan, yes?: boolean, dependencies?: InteractiveDependencies, actionLabel?: string): Promise<boolean>;
78
+ export declare function confirmToolInstallation(prompt: ToolInstallationPrompt | string, dependencies?: InteractiveDependencies): Promise<boolean>;
79
+ export declare function confirmFileReplacements(paths: readonly string[], dependencies?: InteractiveDependencies): Promise<boolean>;
80
+ export declare function confirmDependencyInstallation(commands: DependencyCommandPlan[], dependencies?: InteractiveDependencies): Promise<boolean>;
8
81
  export declare function resolveCatalogInput(options: ProjectOptions): ProjectOptions;
82
+ export {};
@@ -1,52 +1,116 @@
1
+ import { access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { stdin as processInput, stdout as processOutput } from 'node:process';
1
4
  import { createInterface } from 'node:readline/promises';
2
- import { stdin as input, stdout as output } from 'node:process';
3
- import { apiStacks, codingAgents, environments, getApiStack, getCodingAgent, getPattern, getProjectType, getProvider, getSpecWorkflow, patterns, providers, resolveRegion, specWorkflows } from './catalogs.js';
4
- import { formatProjectPlan } from './planner.js';
5
+ import { apiStacks, codingAgents, environments, getApiStack, getCodingAgent, getFrameworkDefinition, getPattern, getProjectType, getProvider, getSpecWorkflow, patterns, projectTypes, providers, resolveRegion, specWorkflows } from './catalogs.js';
6
+ import { projectPlanEntries } from './planner.js';
5
7
  import { formatCommand } from './process-runner.js';
6
- export async function promptForInitOptions(initial) {
7
- const rl = createInterface({ input, output });
8
- try {
9
- const projectName = initial.projectName ?? await askRequired(rl, 'Project name');
8
+ import { PresentationSession } from './terminal.js';
9
+ export class InteractiveCancelledError extends Error {
10
+ constructor(message = 'Interactive initialization was cancelled.') {
11
+ super(message);
12
+ this.name = 'InteractiveCancelledError';
13
+ }
14
+ }
15
+ function errorCode(error) {
16
+ return typeof error === 'object' && error !== null && 'code' in error &&
17
+ typeof error.code === 'string'
18
+ ? error.code
19
+ : undefined;
20
+ }
21
+ function isPromptCancellation(error) {
22
+ return error instanceof Error && (error.name === 'ExitPromptError' ||
23
+ error.name === 'AbortPromptError' ||
24
+ error.message.includes('User force closed the prompt'));
25
+ }
26
+ export class InteractivePrompter {
27
+ input;
28
+ output;
29
+ presentation;
30
+ cwd;
31
+ configuredRoot;
32
+ runner;
33
+ checkboxPrompt;
34
+ rl;
35
+ lines;
36
+ closed = false;
37
+ constructor(dependencies = {}) {
38
+ this.input = dependencies.input ?? processInput;
39
+ this.output = dependencies.output ?? processOutput;
40
+ this.presentation = dependencies.presentation ?? new PresentationSession({
41
+ stdout: this.output,
42
+ stderr: this.output
43
+ });
44
+ this.cwd = dependencies.cwd ?? process.cwd();
45
+ this.configuredRoot = dependencies.configuredRoot;
46
+ this.runner = dependencies.runner;
47
+ this.checkboxPrompt = dependencies.checkboxPrompt;
48
+ }
49
+ close() {
50
+ this.closed = true;
51
+ this.releaseLineInput();
52
+ }
53
+ async promptForInitOptions(initial) {
54
+ const projectName = initial.projectName ?? await this.askRequired('Project name');
10
55
  const inferredProjectType = initial.projectType ?? (initial.pattern ? 'genai' : initial.apiStack ? 'standard' : undefined);
11
- const projectType = inferredProjectType ?? (await confirm(rl, 'Is this a GenAI project?', true) ? 'genai' : 'standard');
56
+ const projectType = inferredProjectType ??
57
+ await this.choose('Select workload', projectTypes.map((workload) => ({
58
+ value: workload.id,
59
+ label: workload.label,
60
+ disabled: false
61
+ })), 'genai');
12
62
  const pattern = projectType === 'genai'
13
- ? initial.pattern ?? await choose(rl, 'Select GenAI pattern', patterns.map((pattern) => ({
63
+ ? initial.pattern ?? await this.choose('Select GenAI pattern', patterns.map((pattern) => ({
14
64
  value: pattern.id,
15
65
  label: `${pattern.label} (${pattern.scaffoldStatus})`,
16
66
  disabled: false
17
67
  })))
18
68
  : initial.pattern;
19
69
  const apiStack = projectType === 'standard'
20
- ? initial.apiStack ?? await choose(rl, 'Select API stack', apiStacks.map((stack) => ({
70
+ ? initial.apiStack ?? await this.choose('Select API stack', apiStacks.map((stack) => ({
21
71
  value: stack.id,
22
72
  label: `${stack.label} (${stack.databaseTooling})`,
23
73
  disabled: false
24
74
  })), 'python-fastapi')
25
- : initial.apiStack ?? 'python-fastapi';
26
- const cloud = initial.cloud ?? await choose(rl, 'Target cloud', providers.map((provider) => ({
27
- value: provider.id,
28
- label: `${provider.label}${provider.status === 'planned' ? ' - planned' : ''}`,
29
- disabled: provider.status === 'planned'
30
- })));
31
- const region = initial.region ?? await promptForRegion(rl, cloud);
32
- const includeFrontend = initial.includeFrontend ?? await confirm(rl, 'Include frontend? (Vue 3 + Tailwind)', false);
33
- const specWorkflow = initial.specWorkflow ?? await choose(rl, 'Select spec-driven workflow', specWorkflows.map((workflow) => ({
75
+ : projectType === 'genai'
76
+ ? initial.apiStack ?? 'python-fastapi'
77
+ : initial.apiStack;
78
+ const cloud = projectType === 'power-apps-code-app'
79
+ ? initial.cloud
80
+ : initial.cloud ?? await this.choose('Target cloud', providers.map((provider) => ({
81
+ value: provider.id,
82
+ label: `${provider.label}${provider.status === 'planned' ? ' - planned' : ''}`,
83
+ disabled: provider.status === 'planned'
84
+ })));
85
+ const region = projectType === 'power-apps-code-app'
86
+ ? initial.region
87
+ : initial.region ?? await this.promptForRegion(cloud);
88
+ const includeFrontend = projectType === 'power-apps-code-app'
89
+ ? initial.includeFrontend
90
+ : initial.includeFrontend ?? await this.confirm('Include frontend? (Vue 3 + Tailwind)', false);
91
+ const specWorkflow = initial.specWorkflow ?? await this.choose('Select spec-driven workflow', specWorkflows.map((workflow) => ({
34
92
  value: workflow.id,
35
- label: `${workflow.label}${workflow.default ? ' (default)' : ''}`,
93
+ label: workflow.label,
36
94
  disabled: false
37
95
  })), 'openspec');
38
- const selectedAgents = initial.agents ?? await askAgents(rl);
96
+ const selectedAgents = initial.agents ?? await this.askAgents(specWorkflow);
39
97
  const normalizedAgents = selectedAgents
40
98
  .map((agent) => getCodingAgent(agent)?.id)
41
99
  .filter((agent) => agent !== undefined);
42
100
  const defaultAgent = specWorkflow === 'spec-kit' && normalizedAgents.length > 1
43
- ? initial.defaultAgent ?? await choose(rl, 'Select the default Spec Kit agent', codingAgents
101
+ ? initial.defaultAgent ?? await this.choose('Select the default Spec Kit agent', codingAgents
44
102
  .filter((agent) => normalizedAgents.includes(agent.id))
45
103
  .map((agent) => ({ value: agent.id, label: agent.label, disabled: false })), normalizedAgents[0])
46
104
  : specWorkflow === 'spec-kit'
47
105
  ? normalizedAgents[0]
48
106
  : undefined;
49
- const selectedEnvironments = initial.environments ?? await askEnvironments(rl);
107
+ const codeAppsPlugin = projectType === 'power-apps-code-app'
108
+ ? initial.codeAppsPlugin ??
109
+ await this.confirm('Include Microsoft Code Apps preview plugin guidance?', false)
110
+ : initial.codeAppsPlugin;
111
+ const selectedEnvironments = projectType === 'power-apps-code-app'
112
+ ? initial.environments
113
+ : initial.environments ?? await this.askEnvironments();
50
114
  return {
51
115
  ...initial,
52
116
  projectName,
@@ -59,158 +123,290 @@ export async function promptForInitOptions(initial) {
59
123
  specWorkflow,
60
124
  agents: normalizedAgents,
61
125
  ...(defaultAgent ? { defaultAgent } : {}),
62
- environments: selectedEnvironments
126
+ ...(selectedEnvironments ? { environments: selectedEnvironments } : {}),
127
+ ...(codeAppsPlugin !== undefined ? { codeAppsPlugin } : {})
63
128
  };
64
129
  }
65
- finally {
66
- rl.close();
130
+ async confirmPlan(plan, yes, actionLabel = 'Initialize project?') {
131
+ this.presentation.definitions('Resolved project plan', projectPlanEntries(plan));
132
+ return yes ? true : this.confirm(actionLabel, true);
67
133
  }
68
- }
69
- export async function confirmPlan(plan, yes) {
70
- if (yes) {
71
- return true;
134
+ async confirmToolInstallation(prompt) {
135
+ if (typeof prompt === 'string') {
136
+ this.presentation.section('Workstation tool requiring action', prompt.split(/\r?\n/));
137
+ }
138
+ else {
139
+ this.presentation.definitions('Workstation tool requiring action', [
140
+ { label: 'Tool', value: `${prompt.label} [${prompt.severity}]` },
141
+ { label: 'Purpose', value: prompt.purpose },
142
+ { label: 'Requirement', value: prompt.requirement },
143
+ { label: 'Observed', value: prompt.observed }
144
+ ]);
145
+ if (prompt.command) {
146
+ this.presentation.command(prompt.command);
147
+ }
148
+ if (prompt.remedy) {
149
+ this.presentation.remedy(prompt.remedy);
150
+ }
151
+ }
152
+ return this.confirm('Run this allowlisted installation command?', false);
72
153
  }
73
- const rl = createInterface({ input, output });
74
- try {
75
- output.write(`\nGenerated plan:\n${formatProjectPlan(plan)}\n\n`);
76
- return await confirm(rl, 'Initialize project?', true);
154
+ async confirmFileReplacements(paths) {
155
+ this.presentation.bullets('Existing regular files with different content', paths);
156
+ return this.confirm('Replace every listed file?', false);
77
157
  }
78
- finally {
79
- rl.close();
158
+ async confirmDependencyInstallation(commands) {
159
+ this.presentation.table('Project dependency commands', ['Purpose', 'Working directory', 'Command'], commands.map((command) => [
160
+ command.label,
161
+ command.cwd,
162
+ formatCommand(command.command)
163
+ ]));
164
+ return this.confirm('Install project-local dependencies now?', false);
80
165
  }
81
- }
82
- async function askRequired(rl, label) {
83
- while (true) {
84
- const answer = (await rl.question(`? ${label}: `)).trim();
85
- if (answer) {
86
- return answer;
166
+ async askRequired(label) {
167
+ while (true) {
168
+ const answer = (await this.question(label)).trim();
169
+ if (answer) {
170
+ return answer;
171
+ }
172
+ this.presentation.warning(`${label} is required.`);
87
173
  }
88
174
  }
89
- }
90
- async function choose(rl, label, options, defaultValue) {
91
- while (true) {
92
- output.write(`? ${label}:\n`);
93
- options.forEach((option, index) => {
94
- output.write(` ${index + 1}. ${option.label}\n`);
95
- });
96
- const answer = (await rl.question('Select option: ')).trim();
97
- if (!answer && defaultValue) {
175
+ async choose(label, options, defaultValue) {
176
+ while (true) {
177
+ this.presentation.choices(label, options.map((option) => ({
178
+ label: option.label,
179
+ value: option.value,
180
+ disabled: option.disabled,
181
+ default: option.value === defaultValue
182
+ })));
183
+ const answer = (await this.question('Select option', defaultValue)).trim();
184
+ if (!answer && defaultValue) {
185
+ return defaultValue;
186
+ }
187
+ const selectedIndex = Number(answer) - 1;
188
+ const selected = options[selectedIndex] ?? options.find((option) => option.value === answer);
189
+ if (!selected) {
190
+ this.presentation.warning('Please choose a valid option.');
191
+ continue;
192
+ }
193
+ if (selected.disabled) {
194
+ this.presentation.warning(`${selected.label} is not available in V1.`);
195
+ continue;
196
+ }
197
+ return selected.value;
198
+ }
199
+ }
200
+ async confirm(label, defaultValue) {
201
+ const suffix = defaultValue ? 'Y/n' : 'y/N';
202
+ const answer = (await this.question(label, suffix)).trim().toLowerCase();
203
+ if (!answer) {
98
204
  return defaultValue;
99
205
  }
100
- const selectedIndex = Number(answer) - 1;
101
- const selected = options[selectedIndex] ?? options.find((option) => option.value === answer);
102
- if (!selected) {
103
- output.write('Please choose a valid option.\n');
104
- continue;
206
+ return answer === 'y' || answer === 'yes';
207
+ }
208
+ async promptForRegion(cloud) {
209
+ const provider = getProvider(cloud);
210
+ if (!provider || provider.id !== 'azure') {
211
+ return '';
105
212
  }
106
- if (selected.disabled) {
107
- output.write(`${selected.label} is not available in V1.\n`);
108
- continue;
213
+ while (true) {
214
+ const answer = (await this.question('Azure region', 'East US / eastus')).trim();
215
+ const resolution = resolveRegion(provider.id, answer);
216
+ if (resolution.status === 'resolved') {
217
+ return resolution.region.slug;
218
+ }
219
+ if (resolution.status === 'ambiguous') {
220
+ const selected = await this.chooseRegion(resolution.matches);
221
+ return selected.slug;
222
+ }
223
+ this.presentation.warning(`Unknown Azure region: ${resolution.input}. Try a region name such as eastus or korea.`);
109
224
  }
110
- return selected.value;
111
225
  }
112
- }
113
- async function confirm(rl, label, defaultValue) {
114
- const suffix = defaultValue ? 'Y/n' : 'y/N';
115
- const answer = (await rl.question(`? ${label} [${suffix}]: `)).trim().toLowerCase();
116
- if (!answer) {
117
- return defaultValue;
226
+ async chooseRegion(matches) {
227
+ this.presentation.table('Matching Azure regions', ['Option', 'Region', 'Identifier'], matches.map((region, index) => [`${index + 1}`, region.displayName, region.slug]));
228
+ while (true) {
229
+ const answer = (await this.question('Select deployment region')).trim();
230
+ const selected = matches[Number(answer) - 1] ??
231
+ matches.find((region) => region.slug === answer);
232
+ if (selected) {
233
+ return selected;
234
+ }
235
+ this.presentation.warning('Please choose a valid deployment region.');
236
+ }
118
237
  }
119
- return answer === 'y' || answer === 'yes';
120
- }
121
- async function promptForRegion(rl, cloud) {
122
- const provider = getProvider(cloud);
123
- if (!provider || provider.id !== 'azure') {
124
- return '';
238
+ async askEnvironments() {
239
+ const answer = (await this.question('Environments', 'dev,test,prod')).trim();
240
+ if (!answer) {
241
+ return environments.map((environment) => environment.id);
242
+ }
243
+ return answer.split(',').map((value) => value.trim()).filter(Boolean);
125
244
  }
126
- while (true) {
127
- const answer = (await rl.question('? Azure region [East US / eastus]: ')).trim();
128
- const resolution = resolveRegion(provider.id, answer);
129
- if (resolution.status === 'resolved') {
130
- return resolution.region.slug;
245
+ async askAgents(specWorkflow) {
246
+ const discovery = await this.discoverAgents(specWorkflow);
247
+ if (this.isRealTty()) {
248
+ return this.askAgentsWithCheckbox(discovery);
131
249
  }
132
- if (resolution.status === 'ambiguous') {
133
- const selected = await chooseRegion(rl, resolution.matches);
134
- return selected.slug;
250
+ return this.askAgentsWithLines(discovery);
251
+ }
252
+ async askAgentsWithLines(discovery) {
253
+ while (true) {
254
+ this.presentation.choices('Select one or more AI coding agents', codingAgents.map((agent) => ({
255
+ label: this.agentChoiceLabel(agent.id, discovery),
256
+ value: agent.id,
257
+ default: discovery.defaults.includes(agent.id),
258
+ selected: discovery.defaults.includes(agent.id)
259
+ })));
260
+ const defaultSelection = discovery.defaults
261
+ .map((agentId) => `${codingAgents.findIndex((agent) => agent.id === agentId) + 1}`)
262
+ .join(',');
263
+ const answer = (await this.question('Select comma-separated options', defaultSelection)).trim() || defaultSelection;
264
+ const selected = answer.split(',').map((value) => value.trim()).filter(Boolean);
265
+ const resolved = selected.map((value) => {
266
+ const byIndex = codingAgents[Number(value) - 1];
267
+ return byIndex ?? getCodingAgent(value);
268
+ });
269
+ if (resolved.some((agent) => !agent)) {
270
+ this.presentation.warning('Please choose valid agent options.');
271
+ continue;
272
+ }
273
+ return codingAgents
274
+ .filter((agent) => resolved.some((selectedAgent) => selectedAgent?.id === agent.id))
275
+ .map((agent) => agent.id);
135
276
  }
136
- output.write(`Unknown Azure region: ${resolution.input}. Try a region name such as eastus or korea.\n`);
137
277
  }
138
- }
139
- async function chooseRegion(rl, matches) {
140
- output.write('Found matching Azure regions:\n');
141
- matches.forEach((region, index) => {
142
- output.write(` ${index + 1}. ${region.displayName} ${region.slug}\n`);
143
- });
144
- while (true) {
145
- const answer = (await rl.question('Select deployment region: ')).trim();
146
- const selected = matches[Number(answer) - 1] ?? matches.find((region) => region.slug === answer);
147
- if (selected) {
148
- return selected;
278
+ async askAgentsWithCheckbox(discovery) {
279
+ this.releaseLineInput();
280
+ try {
281
+ const checkbox = this.checkboxPrompt ?? (await import('@inquirer/prompts')).checkbox;
282
+ const selected = await checkbox({
283
+ message: 'Select one or more AI coding agents',
284
+ choices: codingAgents.map((agent) => ({
285
+ name: this.agentChoiceLabel(agent.id, discovery),
286
+ value: agent.id,
287
+ checked: discovery.defaults.includes(agent.id)
288
+ })),
289
+ required: true,
290
+ validate: (values) => values.length > 0 || 'Select at least one AI coding agent.'
291
+ }, {
292
+ input: this.input,
293
+ output: this.output
294
+ });
295
+ return codingAgents
296
+ .filter((agent) => selected.includes(agent.id))
297
+ .map((agent) => agent.id);
298
+ }
299
+ catch (error) {
300
+ if (isPromptCancellation(error)) {
301
+ throw new InteractiveCancelledError();
302
+ }
303
+ throw error;
149
304
  }
150
305
  }
151
- }
152
- async function askEnvironments(rl) {
153
- const answer = (await rl.question('? Environments [dev,test,prod]: ')).trim();
154
- if (!answer) {
155
- return environments.map((environment) => environment.id);
306
+ async discoverAgents(specWorkflow) {
307
+ const configured = new Set();
308
+ const detected = new Set();
309
+ const framework = getFrameworkDefinition(specWorkflow);
310
+ await Promise.all(codingAgents.map(async (agent) => {
311
+ if (this.configuredRoot) {
312
+ const markerStates = await Promise.all(framework.agentMarkers[agent.id].map(async (pathParts) => this.pathExists(path.join(this.configuredRoot, ...pathParts))));
313
+ if (markerStates.some(Boolean)) {
314
+ configured.add(agent.id);
315
+ }
316
+ }
317
+ if (this.runner) {
318
+ const probe = await this.runner.run({ executable: agent.executable, args: ['--version'] }, { cwd: this.cwd, timeoutMs: 10_000 });
319
+ if (probe.status === 0 && !probe.timedOut) {
320
+ detected.add(agent.id);
321
+ }
322
+ }
323
+ }));
324
+ const defaults = codingAgents
325
+ .filter((agent) => configured.size > 0
326
+ ? configured.has(agent.id)
327
+ : detected.size > 0
328
+ ? detected.has(agent.id)
329
+ : agent.id === 'github-copilot')
330
+ .map((agent) => agent.id);
331
+ return { configured, detected, defaults };
156
332
  }
157
- return answer.split(',').map((value) => value.trim()).filter(Boolean);
158
- }
159
- async function askAgents(rl) {
160
- while (true) {
161
- output.write('? Select one or more AI coding agents:\n');
162
- codingAgents.forEach((agent, index) => {
163
- output.write(` ${index + 1}. ${agent.label}${agent.id === 'github-copilot' ? ' (default)' : ''}\n`);
164
- });
165
- const answer = (await rl.question('Select comma-separated options [1]: ')).trim() || '1';
166
- const selected = answer.split(',').map((value) => value.trim()).filter(Boolean);
167
- const resolved = selected.map((value) => {
168
- const byIndex = codingAgents[Number(value) - 1];
169
- return byIndex ?? getCodingAgent(value);
170
- });
171
- if (resolved.some((agent) => !agent)) {
172
- output.write('Please choose valid agent options.\n');
173
- continue;
333
+ async pathExists(file) {
334
+ try {
335
+ await access(file);
336
+ return true;
337
+ }
338
+ catch (error) {
339
+ if (errorCode(error) === 'ENOENT' || errorCode(error) === 'ENOTDIR') {
340
+ return false;
341
+ }
342
+ throw error;
174
343
  }
175
- return codingAgents
176
- .filter((agent) => resolved.some((selectedAgent) => selectedAgent?.id === agent.id))
177
- .map((agent) => agent.id);
178
344
  }
179
- }
180
- export async function confirmToolInstallation(detail) {
181
- const rl = createInterface({ input, output });
182
- try {
183
- output.write(`\nWorkstation tool requiring action:\n${detail}\n\n`);
184
- return await confirm(rl, 'Run this allowlisted installation command?', false);
345
+ agentChoiceLabel(agentId, discovery) {
346
+ const agent = codingAgents.find((candidate) => candidate.id === agentId);
347
+ if (!agent) {
348
+ throw new Error(`Unknown coding agent: ${agentId}`);
349
+ }
350
+ const states = [
351
+ discovery.configured.has(agentId) ? 'configured' : undefined,
352
+ discovery.detected.has(agentId) ? 'detected' : undefined
353
+ ].filter((state) => state !== undefined);
354
+ return `${agent.label} (${states.length > 0 ? states.join(', ') : 'not observable'})`;
185
355
  }
186
- finally {
187
- rl.close();
356
+ isRealTty() {
357
+ const input = this.input;
358
+ const output = this.output;
359
+ return input.isTTY === true && typeof input.setRawMode === 'function' && output.isTTY === true;
188
360
  }
189
- }
190
- export async function confirmFileReplacements(paths) {
191
- const rl = createInterface({ input, output });
192
- try {
193
- output.write(`\nExisting regular files with different content:\n${paths.map((file) => `- ${file}`).join('\n')}\n\n`);
194
- return await confirm(rl, 'Replace every listed file?', false);
361
+ async question(label, defaultValue) {
362
+ this.presentation.prompt(label, defaultValue);
363
+ const answer = await this.lineIterator().next();
364
+ if (answer.done) {
365
+ throw new InteractiveCancelledError(`Interactive input closed before answering: ${label}.`);
366
+ }
367
+ return answer.value;
195
368
  }
196
- finally {
197
- rl.close();
369
+ lineIterator() {
370
+ if (this.closed) {
371
+ throw new InteractiveCancelledError();
372
+ }
373
+ if (!this.rl || !this.lines) {
374
+ this.rl = createInterface({ input: this.input, terminal: false });
375
+ this.rl.on('SIGINT', () => this.rl?.close());
376
+ this.lines = this.rl[Symbol.asyncIterator]();
377
+ }
378
+ return this.lines;
379
+ }
380
+ releaseLineInput() {
381
+ this.rl?.close();
382
+ this.rl = undefined;
383
+ this.lines = undefined;
198
384
  }
199
385
  }
200
- export async function confirmDependencyInstallation(commands) {
201
- const rl = createInterface({ input, output });
386
+ async function withPrompter(dependencies, callback) {
387
+ const prompter = new InteractivePrompter(dependencies);
202
388
  try {
203
- output.write('\nProject dependency commands:\n');
204
- for (const command of commands) {
205
- output.write(`- ${command.cwd}: ${formatCommand(command.command)}\n`);
206
- }
207
- output.write('\n');
208
- return await confirm(rl, 'Install project-local dependencies now?', false);
389
+ return await callback(prompter);
209
390
  }
210
391
  finally {
211
- rl.close();
392
+ prompter.close();
212
393
  }
213
394
  }
395
+ export async function promptForInitOptions(initial, dependencies) {
396
+ return withPrompter(dependencies, (prompter) => prompter.promptForInitOptions(initial));
397
+ }
398
+ export async function confirmPlan(plan, yes, dependencies, actionLabel) {
399
+ return withPrompter(dependencies, (prompter) => prompter.confirmPlan(plan, yes, actionLabel));
400
+ }
401
+ export async function confirmToolInstallation(prompt, dependencies) {
402
+ return withPrompter(dependencies, (prompter) => prompter.confirmToolInstallation(prompt));
403
+ }
404
+ export async function confirmFileReplacements(paths, dependencies) {
405
+ return withPrompter(dependencies, (prompter) => prompter.confirmFileReplacements(paths));
406
+ }
407
+ export async function confirmDependencyInstallation(commands, dependencies) {
408
+ return withPrompter(dependencies, (prompter) => prompter.confirmDependencyInstallation(commands));
409
+ }
214
410
  export function resolveCatalogInput(options) {
215
411
  return {
216
412
  ...options,