@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.
- package/README.md +65 -224
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +21 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +520 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +27 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +25 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +22 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +23 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +13 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +6597 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +60 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +55 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +17 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +1 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +37 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +46 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +60 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +211 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +92 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +355 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +32 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +184 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +141 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +257 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +21 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +22 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +46 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +29 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +185 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +26 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +13 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +114 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +66 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +18 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +61 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +11 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +131 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +6 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +10 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +31 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +59 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +14 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +26 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +23 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +64 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +24 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +34 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +13 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +26 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +19 -0
- package/assets/power-apps-code-app/README.md +27 -0
- package/dist/args.d.ts +33 -0
- package/dist/args.js +100 -38
- package/dist/args.js.map +1 -1
- package/dist/catalogs.d.ts +3 -1
- package/dist/catalogs.js +23 -0
- package/dist/catalogs.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/code-apps-plugin.d.ts +13 -0
- package/dist/code-apps-plugin.js +69 -0
- package/dist/code-apps-plugin.js.map +1 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +669 -321
- package/dist/commands.js.map +1 -1
- package/dist/file-system.d.ts +16 -1
- package/dist/file-system.js +320 -10
- package/dist/file-system.js.map +1 -1
- package/dist/framework-adapters.d.ts +5 -2
- package/dist/framework-adapters.js +5 -2
- package/dist/framework-adapters.js.map +1 -1
- package/dist/genai-templates.d.ts +6 -6
- package/dist/genai-templates.js +0 -3
- package/dist/genai-templates.js.map +1 -1
- package/dist/interactive.d.ts +80 -6
- package/dist/interactive.js +338 -142
- package/dist/interactive.js.map +1 -1
- package/dist/migrate-plan.d.ts +4 -4
- package/dist/migrate-plan.js +1 -1
- package/dist/migrate-plan.js.map +1 -1
- package/dist/planner.d.ts +5 -0
- package/dist/planner.js +197 -53
- package/dist/planner.js.map +1 -1
- package/dist/power-apps-assets.d.ts +30 -0
- package/dist/power-apps-assets.js +131 -0
- package/dist/power-apps-assets.js.map +1 -0
- package/dist/power-apps-templates.d.ts +3 -0
- package/dist/power-apps-templates.js +140 -0
- package/dist/power-apps-templates.js.map +1 -0
- package/dist/power-apps-validation.d.ts +1 -0
- package/dist/power-apps-validation.js +79 -0
- package/dist/power-apps-validation.js.map +1 -0
- package/dist/process-runner.js +9 -6
- package/dist/process-runner.js.map +1 -1
- package/dist/project-dependencies.d.ts +7 -4
- package/dist/project-dependencies.js +16 -4
- package/dist/project-dependencies.js.map +1 -1
- package/dist/published-verifier.js +1 -1
- package/dist/published-verifier.js.map +1 -1
- package/dist/standard-templates.d.ts +4 -4
- package/dist/standard-templates.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +196 -50
- package/dist/templates.js.map +1 -1
- package/dist/terminal.d.ts +99 -3
- package/dist/terminal.js +482 -66
- package/dist/terminal.js.map +1 -1
- package/dist/types.d.ts +73 -15
- package/dist/workstation.d.ts +10 -5
- package/dist/workstation.js +25 -10
- package/dist/workstation.js.map +1 -1
- package/docs/assets/liftoff-terminal.svg +33 -0
- package/docs/azure-deployment.md +66 -0
- package/docs/cli-reference.md +128 -0
- package/docs/configuration-and-manifests.md +102 -0
- package/docs/existing-repositories.md +87 -0
- package/docs/getting-started.md +114 -0
- package/docs/prerequisites.md +99 -0
- package/docs/project-structure.md +119 -0
- package/docs/safety-and-consent.md +98 -0
- package/docs/spec-workflows-and-agents.md +92 -0
- package/docs/troubleshooting.md +133 -0
- package/docs/workloads.md +128 -0
- package/package.json +8 -2
package/dist/commands.js
CHANGED
|
@@ -4,28 +4,41 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from 'node:fs/promises';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { getCommandHelp, getGeneralHelp, readBooleanFlag, readListFlag, readStringFlag } from './args.js';
|
|
8
8
|
import { getCodingAgent, getFrameworkDefinition, getSpecWorkflow, listRegions, patterns, providers, searchRegions } from './catalogs.js';
|
|
9
|
+
import { probeCodeAppsPlugin } from './code-apps-plugin.js';
|
|
9
10
|
import { initializeFramework } from './framework-adapters.js';
|
|
10
11
|
import { validateFrameworkInstallation } from './framework-validation.js';
|
|
11
|
-
import { artifactPath,
|
|
12
|
-
import {
|
|
12
|
+
import { artifactPath, applyProjectFileTransaction, assertNewOrEmptyDirectory, findProjectRoot, loadManifest, manifestDisplayPath, resolveProjectPath, validateGeneratedProject, writeArtifacts, writeProjectFile } from './file-system.js';
|
|
13
|
+
import { InteractiveCancelledError, InteractivePrompter } from './interactive.js';
|
|
13
14
|
import { applyMergePreflight, assertSafeInitTarget, authorizeMergePreflight, buildMergePreflight, captureTreeState, discoverGitRoot, resolveInitTargetFromDiscovery, validateStagedTree, withStagingArea, writeStagedArtifacts } from './init-filesystem.js';
|
|
14
15
|
import { renderMigrationChecklist, renderMigrationProposal, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
|
|
15
|
-
import { buildProjectPlan,
|
|
16
|
-
import { buildDependencySetupPlan, runDependencySetup } from './project-dependencies.js';
|
|
16
|
+
import { buildProjectPlan, loadConfigOptions, mergeOptions, PlanValidationError, projectPlanEntries } from './planner.js';
|
|
17
|
+
import { buildDependencySetupPlan, dependencyResumeCommand, runDependencySetup, verifyPowerAppsPackageMetadata } from './project-dependencies.js';
|
|
17
18
|
import { formatCommand, NodeCommandRunner } from './process-runner.js';
|
|
18
19
|
import { scanDefaults, scanLegacyProject } from './scan.js';
|
|
19
20
|
import { hasDrift, reconcileProject } from './reconcile.js';
|
|
20
21
|
import { compareSemver } from './semver.js';
|
|
21
22
|
import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
|
|
22
|
-
import {
|
|
23
|
+
import { PresentationSession } from './terminal.js';
|
|
23
24
|
import { liftoffVersion } from './version.js';
|
|
24
25
|
import { blockingReadinessFailures, detectHostEnvironment, installRequirement, probeWorkstation, selectLiftoffRuntimeRequirements, selectWorkstationRequirements } from './workstation.js';
|
|
25
26
|
export async function runCommand(parsed, context) {
|
|
27
|
+
const helpRequested = parsed.command !== undefined && readBooleanFlag(parsed.flags, 'help') === true;
|
|
28
|
+
const jsonMode = !helpRequested && (parsed.command === 'doctor' ||
|
|
29
|
+
parsed.command === 'update' ||
|
|
30
|
+
parsed.command === 'validate') &&
|
|
31
|
+
readBooleanFlag(parsed.flags, 'json') === true;
|
|
32
|
+
const presentation = new PresentationSession({
|
|
33
|
+
stdout: context.stdout,
|
|
34
|
+
stderr: context.stderr,
|
|
35
|
+
...context.terminal,
|
|
36
|
+
json: jsonMode
|
|
37
|
+
});
|
|
38
|
+
const executionContext = { ...context, presentation };
|
|
26
39
|
try {
|
|
27
40
|
if (parsed.command && readBooleanFlag(parsed.flags, 'help')) {
|
|
28
|
-
|
|
41
|
+
renderCommandHelp(parsed.command, presentation);
|
|
29
42
|
return 0;
|
|
30
43
|
}
|
|
31
44
|
switch (parsed.command) {
|
|
@@ -33,156 +46,220 @@ export async function runCommand(parsed, context) {
|
|
|
33
46
|
case 'help':
|
|
34
47
|
case '--help':
|
|
35
48
|
if (parsed.positional[0]) {
|
|
36
|
-
|
|
49
|
+
renderCommandHelp(parsed.positional[0], presentation);
|
|
37
50
|
}
|
|
38
51
|
else {
|
|
39
|
-
|
|
52
|
+
renderGeneralHelp(presentation);
|
|
40
53
|
}
|
|
41
54
|
return 0;
|
|
42
55
|
case 'version':
|
|
43
|
-
|
|
56
|
+
presentation.rawStdout(`Liftoff ${liftoffVersion}\n`);
|
|
44
57
|
return 0;
|
|
45
58
|
case 'init':
|
|
46
|
-
return await initCommand(parsed,
|
|
59
|
+
return await initCommand(parsed, executionContext);
|
|
47
60
|
case 'plan':
|
|
48
|
-
return await planCommand(parsed,
|
|
61
|
+
return await planCommand(parsed, executionContext);
|
|
49
62
|
case 'patterns':
|
|
50
|
-
return patternsCommand(
|
|
63
|
+
return patternsCommand(executionContext);
|
|
51
64
|
case 'providers':
|
|
52
|
-
return providersCommand(
|
|
65
|
+
return providersCommand(executionContext);
|
|
53
66
|
case 'regions':
|
|
54
|
-
return regionsCommand(parsed,
|
|
67
|
+
return regionsCommand(parsed, executionContext);
|
|
55
68
|
case 'validate':
|
|
56
|
-
return await validateCommand(parsed,
|
|
69
|
+
return await validateCommand(parsed, executionContext);
|
|
57
70
|
case 'update':
|
|
58
|
-
return await updateCommand(parsed,
|
|
71
|
+
return await updateCommand(parsed, executionContext);
|
|
59
72
|
case 'migrate':
|
|
60
|
-
return await migrateCommand(parsed,
|
|
73
|
+
return await migrateCommand(parsed, executionContext);
|
|
61
74
|
case 'doctor':
|
|
62
|
-
return await doctorCommand(parsed,
|
|
75
|
+
return await doctorCommand(parsed, executionContext);
|
|
63
76
|
case 'dev':
|
|
64
|
-
return helperCommand(parsed,
|
|
77
|
+
return helperCommand(parsed, executionContext, 'docker compose');
|
|
65
78
|
case 'infra':
|
|
66
|
-
return helperCommand(parsed,
|
|
79
|
+
return helperCommand(parsed, executionContext, 'tofu');
|
|
67
80
|
default:
|
|
68
|
-
|
|
69
|
-
printHelp(context.stderr);
|
|
81
|
+
presentation.error(`Unknown command: ${parsed.command}`, 'Run `liftoff help` to list available commands.');
|
|
70
82
|
return 1;
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
catch (error) {
|
|
86
|
+
if (error instanceof InteractiveCancelledError) {
|
|
87
|
+
presentation.cancellation('Interactive operation stopped.');
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
74
90
|
if (error instanceof PlanValidationError) {
|
|
75
|
-
|
|
91
|
+
presentation.error(error.issues.join('\n'), parsed.command ? `Run \`liftoff ${parsed.command} --help\` to review accepted values.` : undefined);
|
|
76
92
|
return 1;
|
|
77
93
|
}
|
|
78
|
-
|
|
94
|
+
presentation.error(error instanceof Error ? error.message : String(error));
|
|
79
95
|
return 1;
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
async function initCommand(parsed, context) {
|
|
99
|
+
const { presentation } = context;
|
|
100
|
+
presentation.identity('Initialize the project and prepare its workstation');
|
|
83
101
|
const runner = context.runner ?? new NodeCommandRunner();
|
|
102
|
+
presentation.stage('Discover project context');
|
|
84
103
|
const git = await discoverGitRoot(context.cwd, runner);
|
|
85
104
|
let initial = await optionsFromParsedArgs(parsed, context.cwd, true);
|
|
86
105
|
if (!initial.projectName && git.exact && git.root) {
|
|
87
106
|
initial = { ...initial, projectName: path.basename(git.root) };
|
|
88
107
|
}
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
108
|
+
const interactive = initial.yes !== true;
|
|
109
|
+
const prompter = interactive
|
|
110
|
+
? new InteractivePrompter({
|
|
111
|
+
input: context.stdin,
|
|
112
|
+
output: context.stdout,
|
|
113
|
+
presentation,
|
|
114
|
+
cwd: context.cwd,
|
|
115
|
+
configuredRoot: git.exact ? git.root : undefined,
|
|
116
|
+
runner
|
|
117
|
+
})
|
|
118
|
+
: undefined;
|
|
119
|
+
try {
|
|
120
|
+
const needsPrompts = interactive && hasMissingInitInputs(initial);
|
|
121
|
+
if (needsPrompts) {
|
|
122
|
+
presentation.stage('Configure project');
|
|
123
|
+
}
|
|
124
|
+
let options;
|
|
125
|
+
let plan;
|
|
126
|
+
let confirmed;
|
|
127
|
+
try {
|
|
128
|
+
options = needsPrompts ? await prompter.promptForInitOptions(initial) : initial;
|
|
129
|
+
plan = buildProjectPlan(options, { requireProjectName: true });
|
|
130
|
+
presentation.stage('Review resolved plan');
|
|
131
|
+
confirmed = options.yes === true
|
|
132
|
+
? (presentation.definitions('Resolved project plan', projectPlanEntries(plan)), true)
|
|
133
|
+
: await prompter.confirmPlan(plan);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof InteractiveCancelledError) {
|
|
137
|
+
presentation.cancellation('Initialization stopped; no destination files were changed.');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
if (!confirmed) {
|
|
143
|
+
presentation.cancellation('Initialization stopped; no destination files were changed.');
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
presentation.stage('Resolve destination');
|
|
147
|
+
const target = resolveInitTargetFromDiscovery(git, plan.safeProjectName);
|
|
148
|
+
await assertSafeInitTarget(target, target.mode === 'named-child' ? git.canonicalCwd : undefined);
|
|
149
|
+
presentation.stage('Check workstation readiness');
|
|
150
|
+
const readiness = await ensureWorkstationReady(plan, options, context, runner, presentation, prompter);
|
|
151
|
+
if (!readiness.ready) {
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
presentation.stage('Stage project files');
|
|
155
|
+
const staged = await withStagingArea(async (area) => {
|
|
156
|
+
const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
|
|
157
|
+
await writeStagedArtifacts(area, partition.durable, 'liftoff');
|
|
158
|
+
presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
|
|
159
|
+
await initializeFramework(area, plan, runner, {
|
|
160
|
+
...presentation.childStreams(),
|
|
161
|
+
onCommand: (command) => presentation.command(command)
|
|
162
|
+
});
|
|
163
|
+
await writeStagedArtifacts(area, partition.seed, 'seed');
|
|
164
|
+
await writeStagedArtifacts(area, [partition.manifest], 'liftoff');
|
|
165
|
+
presentation.stage('Validate staged project');
|
|
166
|
+
await validateStagedTree(area);
|
|
167
|
+
const stagedIssues = await validateGeneratedProject(area.root);
|
|
168
|
+
if (stagedIssues.length > 0) {
|
|
169
|
+
throw new Error(`Staged project validation failed:\n${stagedIssues.join('\n')}`);
|
|
170
|
+
}
|
|
171
|
+
const preflight = await buildMergePreflight(area, target.root);
|
|
172
|
+
const authorized = await authorizeMergePreflight(preflight, options.force === true, interactive ? (paths) => prompter.confirmFileReplacements(paths) : undefined);
|
|
173
|
+
if (!authorized) {
|
|
174
|
+
return {
|
|
175
|
+
status: interactive ? 'declined' : 'authorization-required'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
presentation.stage('Merge staged project', target.root);
|
|
179
|
+
return { status: 'applied', merge: await applyMergePreflight(authorized) };
|
|
180
|
+
});
|
|
181
|
+
if (staged.status === 'declined') {
|
|
182
|
+
presentation.cancellation('No destination files were changed.');
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
if (staged.status === 'authorization-required') {
|
|
186
|
+
presentation.error('Existing regular-file conflicts require --force in non-interactive mode.', 'Review the listed conflicts, then rerun with `--force` only if every replacement is intended.');
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
const issues = await validateGeneratedProject(target.root);
|
|
190
|
+
if (issues.length > 0) {
|
|
191
|
+
presentation.error(`Initialized project validation failed:\n${issues.join('\n')}`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
const dependencyPhase = await handleProjectDependencies(plan, target.root, options, readiness.probes, context, runner, presentation, prompter);
|
|
195
|
+
if (!dependencyPhase.success) {
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
presentation.bullets('Configured integrations', [
|
|
199
|
+
`${plan.specWorkflow.label} ${plan.framework.version}`,
|
|
200
|
+
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`),
|
|
201
|
+
...readiness.pluginProbes.map((probe) => `Code Apps plugin for ${probe.agent.label}: ${probe.state}`)
|
|
202
|
+
]);
|
|
203
|
+
if (readiness.deferred.length > 0) {
|
|
204
|
+
presentation.bullets('Deferred advisory checks', readiness.deferred);
|
|
205
|
+
}
|
|
206
|
+
if (dependencyPhase.deferred.length > 0) {
|
|
207
|
+
presentation.bullets('Deferred project dependencies', dependencyPhase.deferred);
|
|
208
|
+
}
|
|
209
|
+
presentation.completion(`Initialized ${plan.projectName}`, target.root, [
|
|
210
|
+
{ label: 'Target', value: target.root },
|
|
211
|
+
{ label: 'Spec workflow', value: plan.specWorkflow.label },
|
|
212
|
+
{ label: 'Coding agents', value: plan.agents.map((agent) => agent.label).join(', ') }
|
|
213
|
+
], `liftoff validate ${JSON.stringify(target.root)}`);
|
|
128
214
|
return 0;
|
|
129
215
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return 1;
|
|
133
|
-
}
|
|
134
|
-
const issues = await validateGeneratedProject(target.root);
|
|
135
|
-
if (issues.length > 0) {
|
|
136
|
-
context.stderr.write(`Initialized project validation failed:\n${issues.join('\n')}\n`);
|
|
137
|
-
return 1;
|
|
138
|
-
}
|
|
139
|
-
const dependencyPhase = await handleProjectDependencies(plan, target.root, options, readiness.probes, context, runner, renderer);
|
|
140
|
-
if (!dependencyPhase.success) {
|
|
141
|
-
return 1;
|
|
142
|
-
}
|
|
143
|
-
renderer.write(renderer.status('success', `Initialized ${plan.projectName}`, target.root));
|
|
144
|
-
renderer.write(renderer.panel('Configured integrations', [
|
|
145
|
-
`${plan.specWorkflow.label} ${plan.framework.version}`,
|
|
146
|
-
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`)
|
|
147
|
-
]));
|
|
148
|
-
if (readiness.deferred.length > 0) {
|
|
149
|
-
renderer.write(renderer.panel('Deferred advisory checks', readiness.deferred));
|
|
150
|
-
}
|
|
151
|
-
if (dependencyPhase.deferred.length > 0) {
|
|
152
|
-
renderer.write(renderer.panel('Deferred project dependencies', dependencyPhase.deferred));
|
|
216
|
+
finally {
|
|
217
|
+
prompter?.close();
|
|
153
218
|
}
|
|
154
|
-
renderer.write(renderer.command(`liftoff validate ${JSON.stringify(target.root)}`));
|
|
155
|
-
return 0;
|
|
156
219
|
}
|
|
157
220
|
async function planCommand(parsed, context) {
|
|
221
|
+
const { presentation } = context;
|
|
222
|
+
presentation.identity('Preview project decisions, artifacts, and workstation requirements');
|
|
158
223
|
const options = await optionsFromParsedArgs(parsed, context.cwd, false);
|
|
159
224
|
const plan = buildProjectPlan(options, { requireProjectName: false });
|
|
160
225
|
const artifacts = buildArtifacts(plan);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (const requirement of selectWorkstationRequirements(plan)) {
|
|
167
|
-
const version = requirement.exactVersion
|
|
226
|
+
presentation.definitions('Project decisions', projectPlanEntries(plan));
|
|
227
|
+
presentation.table(`Artifacts (${artifacts.length})`, ['Artifact', 'Path'], artifacts.map((artifact) => [artifact.logicalName, artifact.pathParts.join('/')]));
|
|
228
|
+
const workstationRows = selectWorkstationRequirements(plan).map((requirement) => [
|
|
229
|
+
requirement.definition.label,
|
|
230
|
+
requirement.exactVersion
|
|
168
231
|
? `exactly ${requirement.exactVersion}`
|
|
169
232
|
: requirement.minimumVersion
|
|
170
233
|
? `${requirement.minimumVersion}+`
|
|
171
|
-
: 'available'
|
|
172
|
-
|
|
234
|
+
: 'available',
|
|
235
|
+
requirement.severity
|
|
236
|
+
]);
|
|
237
|
+
if (presentation.stdout.layout === 'plain') {
|
|
238
|
+
presentation.section('Workstation requirements', workstationRows.map(([label, version, severity]) => `${label}: ${version} [${severity}]`));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
presentation.table('Workstation requirements', ['Requirement', 'Version', 'Level'], workstationRows);
|
|
173
242
|
}
|
|
174
243
|
return 0;
|
|
175
244
|
}
|
|
176
|
-
async function ensureWorkstationReady(plan, options, context, runner,
|
|
245
|
+
async function ensureWorkstationReady(plan, options, context, runner, presentation, prompter, resumeInvocation = 'liftoff init', commandCwd) {
|
|
177
246
|
const requirements = selectWorkstationRequirements(plan);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
247
|
+
const [initialProbes, pluginProbes] = await Promise.all([
|
|
248
|
+
probeWorkstation(requirements, runner, { cwd: commandCwd }),
|
|
249
|
+
plan.workload === 'power-apps-code-app' && plan.codeAppsPlugin
|
|
250
|
+
? probeCodeAppsPlugin(plan.agents, runner, commandCwd)
|
|
251
|
+
: Promise.resolve([])
|
|
252
|
+
]);
|
|
253
|
+
let probes = initialProbes;
|
|
254
|
+
presentation.table('Workstation readiness', ['Requirement', 'Level', 'State', 'Detail'], probes.map((probe) => [
|
|
181
255
|
probe.requirement.definition.label,
|
|
182
256
|
probe.requirement.severity,
|
|
183
257
|
probe.state,
|
|
184
258
|
probe.detail
|
|
185
|
-
]))
|
|
259
|
+
]));
|
|
260
|
+
if (pluginProbes.length > 0) {
|
|
261
|
+
presentation.table('Optional Code Apps plugin', ['Agent', 'State', 'Detail'], pluginProbes.map((probe) => [probe.agent.label, probe.state, probe.detail]));
|
|
262
|
+
}
|
|
186
263
|
const actionable = probes.filter((probe) => probe.state !== 'ready');
|
|
187
264
|
const host = await detectHostEnvironment();
|
|
188
265
|
const installInstruction = (probe) => {
|
|
@@ -213,17 +290,16 @@ async function ensureWorkstationReady(plan, options, context, runner, renderer,
|
|
|
213
290
|
: probe.requirement.minimumVersion
|
|
214
291
|
? `required ${probe.requirement.minimumVersion} or newer`
|
|
215
292
|
: 'required to be available';
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (await confirmToolInstallation(detail)) {
|
|
293
|
+
if (await prompter.confirmToolInstallation({
|
|
294
|
+
label: probe.requirement.definition.label,
|
|
295
|
+
severity: probe.requirement.severity,
|
|
296
|
+
purpose: probe.requirement.reasons.join('; '),
|
|
297
|
+
requirement: constraint,
|
|
298
|
+
observed: `${probe.state} - ${probe.detail}`,
|
|
299
|
+
...(automatic
|
|
300
|
+
? { command: formatCommand(recipe.command) }
|
|
301
|
+
: { remedy: installInstruction(probe) })
|
|
302
|
+
})) {
|
|
227
303
|
authorizedInstallations.add(probe.requirement.id);
|
|
228
304
|
}
|
|
229
305
|
}
|
|
@@ -234,12 +310,17 @@ async function ensureWorkstationReady(plan, options, context, runner, renderer,
|
|
|
234
310
|
if (!authorizedInstallations.has(probe.requirement.id)) {
|
|
235
311
|
continue;
|
|
236
312
|
}
|
|
313
|
+
presentation.stage(`Install ${probe.requirement.definition.label}`, `${probe.state} - ${probe.detail}`);
|
|
314
|
+
const recipe = probe.requirement.definition.install[host.platform];
|
|
315
|
+
if (recipe && (host.platform !== 'linux' || recipe.manager === 'npm' || recipe.manager === 'uv')) {
|
|
316
|
+
presentation.command(formatCommand(recipe.command));
|
|
317
|
+
}
|
|
237
318
|
const installation = await installRequirement(probe.requirement, probe, {
|
|
238
319
|
authorized: true,
|
|
239
320
|
host,
|
|
240
321
|
runner,
|
|
241
322
|
cwd: commandCwd,
|
|
242
|
-
streamOptions:
|
|
323
|
+
streamOptions: presentation.childStreams()
|
|
243
324
|
});
|
|
244
325
|
updates.set(probe.requirement.id, installation.probe);
|
|
245
326
|
const kind = installation.state === 'installed'
|
|
@@ -247,9 +328,9 @@ async function ensureWorkstationReady(plan, options, context, runner, renderer,
|
|
|
247
328
|
: probe.requirement.severity === 'blocking'
|
|
248
329
|
? 'error'
|
|
249
330
|
: 'warning';
|
|
250
|
-
|
|
331
|
+
presentation.status(kind, probe.requirement.definition.label, installation.detail);
|
|
251
332
|
if (installation.remedy) {
|
|
252
|
-
|
|
333
|
+
presentation.command(installation.remedy);
|
|
253
334
|
}
|
|
254
335
|
}
|
|
255
336
|
probes = probes.map((probe) => updates.get(probe.requirement.id) ?? probe);
|
|
@@ -257,13 +338,12 @@ async function ensureWorkstationReady(plan, options, context, runner, renderer,
|
|
|
257
338
|
const blockers = blockingReadinessFailures(probes);
|
|
258
339
|
if (blockers.length > 0) {
|
|
259
340
|
for (const blocker of blockers) {
|
|
260
|
-
|
|
261
|
-
`\nRemedy: ${installInstruction(blocker)}\n`);
|
|
341
|
+
presentation.error(`${blocker.requirement.definition.label}: ${blocker.detail}`, installInstruction(blocker));
|
|
262
342
|
}
|
|
263
|
-
|
|
264
|
-
? `Open a new terminal if PATH changed, then rerun \`${resumeInvocation}\` with the same project options
|
|
265
|
-
: `Resume with \`${resumeInvocation} --install-tools\` plus the same project options after reviewing the commands
|
|
266
|
-
return { ready: false, deferred: [], probes };
|
|
343
|
+
presentation.error('Workstation readiness is incomplete.', options.installTools
|
|
344
|
+
? `Open a new terminal if PATH changed, then rerun \`${resumeInvocation}\` with the same project options.`
|
|
345
|
+
: `Resume with \`${resumeInvocation} --install-tools\` plus the same project options after reviewing the commands.`);
|
|
346
|
+
return { ready: false, deferred: [], probes, pluginProbes };
|
|
267
347
|
}
|
|
268
348
|
const deferred = [
|
|
269
349
|
...probes
|
|
@@ -271,80 +351,104 @@ async function ensureWorkstationReady(plan, options, context, runner, renderer,
|
|
|
271
351
|
.map((probe) => `${probe.requirement.definition.label}: ${probe.detail} Remedy: ${installInstruction(probe)}`),
|
|
272
352
|
...probes.flatMap((probe) => probe.notices
|
|
273
353
|
.filter((notice) => notice.state !== 'ready')
|
|
274
|
-
.map((notice) => `${notice.label}: ${notice.detail}${notice.remedy ? ` Remedy: ${notice.remedy}` : ''}`))
|
|
354
|
+
.map((notice) => `${notice.label}: ${notice.detail}${notice.remedy ? ` Remedy: ${notice.remedy}` : ''}`)),
|
|
355
|
+
...pluginProbes
|
|
356
|
+
.filter((probe) => probe.state !== 'ready')
|
|
357
|
+
.map((probe) => `${probe.agent.label} Code Apps plugin: ${probe.detail}${probe.remedy ? ` Remedy: ${probe.remedy}` : ''}`)
|
|
275
358
|
];
|
|
276
|
-
return { ready: true, deferred, probes };
|
|
359
|
+
return { ready: true, deferred, probes, pluginProbes };
|
|
277
360
|
}
|
|
278
|
-
async function handleProjectDependencies(plan, projectRoot, options, probes, context, runner,
|
|
361
|
+
async function handleProjectDependencies(plan, projectRoot, options, probes, context, runner, presentation, prompter) {
|
|
279
362
|
const dependencyPlan = buildDependencySetupPlan(plan, projectRoot, probes);
|
|
280
363
|
let installDependencies = options.installDependencies === true;
|
|
281
364
|
if (options.installDependencies === undefined &&
|
|
282
365
|
options.yes !== true &&
|
|
283
366
|
dependencyPlan.commands.length > 0) {
|
|
284
|
-
installDependencies = await confirmDependencyInstallation(dependencyPlan.commands);
|
|
367
|
+
installDependencies = await prompter.confirmDependencyInstallation(dependencyPlan.commands);
|
|
285
368
|
}
|
|
286
369
|
if (!installDependencies) {
|
|
287
370
|
return {
|
|
288
371
|
success: true,
|
|
289
|
-
deferred: dependencyPlan.commands.map((command) => `${command.label}: ${
|
|
372
|
+
deferred: dependencyPlan.commands.map((command) => `${command.label}: ${dependencyResumeCommand(command)}`)
|
|
290
373
|
};
|
|
291
374
|
}
|
|
375
|
+
if (plan.workload === 'power-apps-code-app') {
|
|
376
|
+
const issues = await verifyPowerAppsPackageMetadata(projectRoot);
|
|
377
|
+
if (issues.length > 0) {
|
|
378
|
+
presentation.error('Power Apps dependency installation blocked', `${issues.join(' ')} Restore package.json and package-lock.json or run \`liftoff update --apply\`.`);
|
|
379
|
+
return { success: false, deferred: [] };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
presentation.stage('Install project dependencies');
|
|
292
383
|
const result = await runDependencySetup(dependencyPlan, projectRoot, runner, {
|
|
293
|
-
|
|
294
|
-
|
|
384
|
+
...presentation.childStreams(),
|
|
385
|
+
onCommand: (command) => {
|
|
386
|
+
presentation.status('pending', command.label, command.cwd);
|
|
387
|
+
presentation.command(formatCommand(command.command));
|
|
388
|
+
}
|
|
295
389
|
});
|
|
296
390
|
if (!result.success) {
|
|
297
|
-
|
|
298
|
-
|
|
391
|
+
presentation.error('Project dependencies failed', `${result.failed?.label ?? 'dependency command'}: ${result.detail ?? 'unknown failure'}`);
|
|
392
|
+
presentation.status('info', 'Scaffold preserved', projectRoot);
|
|
299
393
|
if (result.restoredMutations.length > 0) {
|
|
300
|
-
|
|
394
|
+
presentation.warning(`Restored protected files: ${result.restoredMutations.join(', ')}`);
|
|
301
395
|
}
|
|
302
396
|
if (result.resumeCommand) {
|
|
303
|
-
|
|
397
|
+
presentation.command(result.resumeCommand);
|
|
304
398
|
}
|
|
305
399
|
return { success: false, deferred: [] };
|
|
306
400
|
}
|
|
307
|
-
|
|
401
|
+
presentation.status('success', 'Project dependencies', `${result.completed.length} command${result.completed.length === 1 ? '' : 's'} completed`);
|
|
308
402
|
return { success: true, deferred: [] };
|
|
309
403
|
}
|
|
310
404
|
function patternsCommand(context) {
|
|
311
|
-
context.
|
|
312
|
-
|
|
313
|
-
context.stdout.write(`- ${pattern.id}: ${pattern.label} [${pattern.scaffoldStatus}]\n`);
|
|
314
|
-
}
|
|
405
|
+
context.presentation.commandIdentity('patterns', 'Available GenAI application patterns');
|
|
406
|
+
context.presentation.table('Patterns', ['Identifier', 'Pattern', 'Scaffold'], patterns.map((pattern) => [pattern.id, pattern.label, pattern.scaffoldStatus]));
|
|
315
407
|
return 0;
|
|
316
408
|
}
|
|
317
409
|
function providersCommand(context) {
|
|
318
|
-
context.
|
|
319
|
-
|
|
320
|
-
context.stdout.write(`- ${provider.id}: ${provider.label} [${provider.status}]\n`);
|
|
321
|
-
}
|
|
410
|
+
context.presentation.commandIdentity('providers', 'Cloud provider availability');
|
|
411
|
+
context.presentation.table('Providers', ['Identifier', 'Provider', 'Availability'], providers.map((provider) => [provider.id, provider.label, provider.status]));
|
|
322
412
|
return 0;
|
|
323
413
|
}
|
|
324
414
|
function regionsCommand(parsed, context) {
|
|
325
415
|
const cloud = readStringFlag(parsed.flags, 'cloud') ?? 'azure';
|
|
416
|
+
context.presentation.commandIdentity('regions', 'Cloud deployment regions');
|
|
326
417
|
if (cloud !== 'azure') {
|
|
327
|
-
context.
|
|
418
|
+
context.presentation.error(`${cloud} regions are not available until the provider adapter is implemented.`, 'Run `liftoff providers` to review currently available providers.');
|
|
328
419
|
return 1;
|
|
329
420
|
}
|
|
330
421
|
const query = parsed.positional[0] ?? readStringFlag(parsed.flags, 'region');
|
|
331
422
|
const regions = parsed.subcommand === 'search' && query ? searchRegions('azure', query) : listRegions('azure');
|
|
332
|
-
|
|
333
|
-
context.
|
|
423
|
+
if (regions.length === 0) {
|
|
424
|
+
context.presentation.warning(`No Azure regions matched ${JSON.stringify(query ?? '')}.`);
|
|
425
|
+
return 0;
|
|
334
426
|
}
|
|
427
|
+
context.presentation.table(query ? `Azure region matches for ${JSON.stringify(query)}` : 'Azure regions', ['Identifier', 'Region', 'Geography'], regions.map((region) => [region.slug, region.displayName, region.geography]));
|
|
335
428
|
return 0;
|
|
336
429
|
}
|
|
337
430
|
async function validateCommand(parsed, context) {
|
|
431
|
+
context.presentation.commandIdentity('validate', 'Validate a generated Liftoff project');
|
|
432
|
+
const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
|
|
338
433
|
const explicit = parsed.positional[0] ?? readStringFlag(parsed.flags, 'project');
|
|
339
434
|
const projectRoot = explicit
|
|
340
435
|
? path.resolve(context.cwd, explicit)
|
|
341
436
|
: (await findProjectRoot(context.cwd)) ?? context.cwd;
|
|
342
437
|
const issues = await validateGeneratedProject(projectRoot);
|
|
438
|
+
if (jsonMode) {
|
|
439
|
+
context.presentation.rawStdout(`${JSON.stringify({
|
|
440
|
+
schemaVersion: 1,
|
|
441
|
+
projectRoot,
|
|
442
|
+
valid: issues.length === 0,
|
|
443
|
+
issues
|
|
444
|
+
}, null, 2)}\n`);
|
|
445
|
+
return issues.length === 0 ? 0 : 1;
|
|
446
|
+
}
|
|
343
447
|
if (issues.length > 0) {
|
|
344
|
-
context.
|
|
448
|
+
context.presentation.error(issues.join('\n'), 'Restore invalid generated files or the manifest from version control, then rerun validation.');
|
|
345
449
|
return 1;
|
|
346
450
|
}
|
|
347
|
-
context.
|
|
451
|
+
context.presentation.status('success', 'Generated project manifest is valid', projectRoot);
|
|
348
452
|
return 0;
|
|
349
453
|
}
|
|
350
454
|
const STAGING_EXCLUDES = new Set(['.git', 'node_modules', 'vendor', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next']);
|
|
@@ -454,13 +558,16 @@ function migrationPlanArtifacts(plan, inventory) {
|
|
|
454
558
|
};
|
|
455
559
|
}
|
|
456
560
|
async function executeMigration(parsed, context, sourceRoot) {
|
|
561
|
+
const { presentation } = context;
|
|
562
|
+
presentation.stage('Scan legacy project', sourceRoot);
|
|
457
563
|
const inventory = await scanLegacyProject(sourceRoot);
|
|
458
564
|
const { options: defaults, provenance } = scanDefaults(inventory);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
565
|
+
if (presentation.stdout.layout === 'plain') {
|
|
566
|
+
presentation.section('Scan defaults (override in prompts or with flags)', provenance.map((item) => `${item.field}: ${String(item.value)} (detected: ${item.evidence})`));
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
presentation.table('Scan defaults (override in prompts or with flags)', ['Decision', 'Detected value', 'Evidence'], provenance.map((item) => [item.field, String(item.value), item.evidence]));
|
|
462
570
|
}
|
|
463
|
-
context.stdout.write('\n');
|
|
464
571
|
const flagOptions = await optionsFromParsedArgs(parsed, context.cwd, false);
|
|
465
572
|
const initial = mergeOptions(defaults, flagOptions);
|
|
466
573
|
if (flagOptions.pattern && flagOptions.projectType === undefined) {
|
|
@@ -479,97 +586,136 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
479
586
|
if (flagOptions.projectType === 'genai' && flagOptions.apiStack === undefined) {
|
|
480
587
|
initial.apiStack = undefined;
|
|
481
588
|
}
|
|
482
|
-
const needsPrompts = !initial.yes && hasMissingInitInputs(initial);
|
|
483
|
-
const options = needsPrompts ? await promptForInitOptions(initial) : initial;
|
|
484
|
-
const plan = buildProjectPlan(options, { requireProjectName: true });
|
|
485
|
-
const confirmed = await confirmPlan(plan, options.yes);
|
|
486
|
-
if (!confirmed) {
|
|
487
|
-
context.stdout.write('Migration cancelled.\n');
|
|
488
|
-
return 0;
|
|
489
|
-
}
|
|
490
|
-
const parentDir = path.dirname(sourceRoot);
|
|
491
|
-
let targetRoot = path.resolve(parentDir, plan.safeProjectName);
|
|
492
|
-
if (targetRoot === sourceRoot) {
|
|
493
|
-
targetRoot = path.resolve(parentDir, `${plan.safeProjectName}-liftoff`);
|
|
494
|
-
}
|
|
495
|
-
const target = { root: targetRoot, mode: 'named-child' };
|
|
496
|
-
await assertSafeInitTarget(target, parentDir);
|
|
497
|
-
await assertNewOrEmptyDirectory(targetRoot);
|
|
498
589
|
const runner = context.runner ?? new NodeCommandRunner();
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
590
|
+
const interactive = initial.yes !== true;
|
|
591
|
+
const prompter = interactive
|
|
592
|
+
? new InteractivePrompter({
|
|
593
|
+
input: context.stdin,
|
|
594
|
+
output: context.stdout,
|
|
595
|
+
presentation,
|
|
596
|
+
cwd: context.cwd,
|
|
597
|
+
runner
|
|
598
|
+
})
|
|
599
|
+
: undefined;
|
|
600
|
+
try {
|
|
601
|
+
const needsPrompts = interactive && hasMissingInitInputs(initial);
|
|
602
|
+
if (needsPrompts) {
|
|
603
|
+
presentation.stage('Configure migrated project');
|
|
505
604
|
}
|
|
506
|
-
|
|
507
|
-
|
|
605
|
+
const options = needsPrompts ? await prompter.promptForInitOptions(initial) : initial;
|
|
606
|
+
const plan = buildProjectPlan(options, { requireProjectName: true });
|
|
607
|
+
if (plan.workload === 'power-apps-code-app') {
|
|
608
|
+
presentation.error('Power Apps code app migration is not supported.', 'Initialize a fresh Power Apps code app, then move application changes intentionally.');
|
|
609
|
+
return 1;
|
|
508
610
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
611
|
+
presentation.stage('Review migration plan');
|
|
612
|
+
const confirmed = options.yes === true
|
|
613
|
+
? (presentation.definitions('Resolved migration plan', projectPlanEntries(plan)), true)
|
|
614
|
+
: await prompter.confirmPlan(plan, undefined, 'Migrate project?');
|
|
615
|
+
if (!confirmed) {
|
|
616
|
+
presentation.cancellation('Migration stopped; the source and destination were not modified.');
|
|
617
|
+
return 0;
|
|
618
|
+
}
|
|
619
|
+
presentation.stage('Resolve fresh migration target');
|
|
620
|
+
const parentDir = path.dirname(sourceRoot);
|
|
621
|
+
let targetRoot = path.resolve(parentDir, plan.safeProjectName);
|
|
622
|
+
if (targetRoot === sourceRoot) {
|
|
623
|
+
targetRoot = path.resolve(parentDir, `${plan.safeProjectName}-liftoff`);
|
|
624
|
+
}
|
|
625
|
+
const target = { root: targetRoot, mode: 'named-child' };
|
|
626
|
+
await assertSafeInitTarget(target, parentDir);
|
|
627
|
+
await assertNewOrEmptyDirectory(targetRoot);
|
|
628
|
+
presentation.stage('Check workstation readiness');
|
|
629
|
+
const readinessRoot = await mkdtemp(path.join(os.tmpdir(), 'liftoff-migrate-readiness-'));
|
|
630
|
+
const readiness = await (async () => {
|
|
631
|
+
try {
|
|
632
|
+
return await ensureWorkstationReady(plan, options, context, runner, presentation, prompter, `liftoff migrate ${JSON.stringify(sourceRoot)}`, readinessRoot);
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
await rm(readinessRoot, { recursive: true, force: true });
|
|
636
|
+
}
|
|
637
|
+
})();
|
|
638
|
+
if (!readiness.ready) {
|
|
639
|
+
return 1;
|
|
640
|
+
}
|
|
641
|
+
const migrationPlan = migrationPlanArtifacts(plan, inventory);
|
|
642
|
+
presentation.stage('Stage fresh migration project');
|
|
643
|
+
await withStagingArea(async (area) => {
|
|
644
|
+
const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
|
|
645
|
+
await writeStagedArtifacts(area, partition.durable, 'liftoff');
|
|
646
|
+
presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
|
|
647
|
+
await initializeFramework(area, plan, runner, {
|
|
648
|
+
...presentation.childStreams(),
|
|
649
|
+
onCommand: (command) => presentation.command(command)
|
|
650
|
+
});
|
|
651
|
+
await writeStagedArtifacts(area, partition.seed, 'seed');
|
|
652
|
+
presentation.stage('Copy filtered legacy source', sourceRoot);
|
|
653
|
+
await stageMigrationSource(area, sourceRoot);
|
|
654
|
+
await writeStagedArtifacts(area, migrationPlan.artifacts, 'seed');
|
|
655
|
+
await writeStagedArtifacts(area, [partition.manifest], 'liftoff');
|
|
656
|
+
presentation.stage('Validate staged migration');
|
|
657
|
+
await validateStagedTree(area);
|
|
658
|
+
const stagedIssues = await validateGeneratedProject(area.root);
|
|
659
|
+
if (stagedIssues.length > 0) {
|
|
660
|
+
throw new Error(`Staged migration project validation failed:\n${stagedIssues.join('\n')}`);
|
|
661
|
+
}
|
|
662
|
+
const preflight = await buildMergePreflight(area, targetRoot);
|
|
663
|
+
const existing = preflight.entries.filter((entry) => entry.destination.type !== 'missing');
|
|
664
|
+
if (existing.length > 0) {
|
|
665
|
+
throw new Error(`Migration target must remain new or empty; --force cannot replace existing content:\n` +
|
|
666
|
+
existing.map((entry) => `- ${entry.relativePath}`).join('\n'));
|
|
667
|
+
}
|
|
668
|
+
const authorized = await authorizeMergePreflight(preflight, false);
|
|
669
|
+
if (!authorized) {
|
|
670
|
+
throw new Error('Migration target authorization failed.');
|
|
671
|
+
}
|
|
672
|
+
presentation.stage('Merge fresh migration target', targetRoot);
|
|
673
|
+
await applyMergePreflight(authorized, { requireEmptyTarget: true });
|
|
520
674
|
});
|
|
521
|
-
await
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
renderer.write(renderer.status('success', `Migrated ${plan.projectName}`, targetRoot));
|
|
552
|
-
renderer.write(renderer.panel('Configured integrations', [
|
|
553
|
-
`${plan.specWorkflow.label} ${plan.framework.version}`,
|
|
554
|
-
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`)
|
|
555
|
-
]));
|
|
556
|
-
if (readiness.deferred.length > 0) {
|
|
557
|
-
renderer.write(renderer.panel('Deferred advisory checks', readiness.deferred));
|
|
675
|
+
const issues = await validateGeneratedProject(targetRoot);
|
|
676
|
+
if (issues.length > 0) {
|
|
677
|
+
presentation.error(`Migrated project validation failed:\n${issues.join('\n')}`);
|
|
678
|
+
return 1;
|
|
679
|
+
}
|
|
680
|
+
const dependencyPhase = await handleProjectDependencies(plan, targetRoot, options, readiness.probes, context, runner, presentation, prompter);
|
|
681
|
+
if (!dependencyPhase.success) {
|
|
682
|
+
return 1;
|
|
683
|
+
}
|
|
684
|
+
presentation.bullets('Configured integrations', [
|
|
685
|
+
`${plan.specWorkflow.label} ${plan.framework.version}`,
|
|
686
|
+
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`)
|
|
687
|
+
]);
|
|
688
|
+
if (readiness.deferred.length > 0) {
|
|
689
|
+
presentation.bullets('Deferred advisory checks', readiness.deferred);
|
|
690
|
+
}
|
|
691
|
+
if (dependencyPhase.deferred.length > 0) {
|
|
692
|
+
presentation.bullets('Deferred project dependencies', dependencyPhase.deferred);
|
|
693
|
+
}
|
|
694
|
+
presentation.bullets('Next steps', [
|
|
695
|
+
`Optional - preserve history: copy the .git directory from ${sourceRoot} into ${targetRoot}, then commit the migration on top (git rename detection preserves file history).`,
|
|
696
|
+
`Execute the migration plan: ${migrationPlan.location}`,
|
|
697
|
+
'Verify compliance: liftoff validate && liftoff doctor'
|
|
698
|
+
]);
|
|
699
|
+
presentation.completion(`Migrated ${plan.projectName}`, targetRoot, [
|
|
700
|
+
{ label: 'Target', value: targetRoot },
|
|
701
|
+
{ label: 'Source', value: `${sourceRoot} (not modified)` },
|
|
702
|
+
{ label: 'Rollback', value: `Delete ${targetRoot}` }
|
|
703
|
+
], 'liftoff validate && liftoff doctor');
|
|
704
|
+
return 0;
|
|
558
705
|
}
|
|
559
|
-
|
|
560
|
-
|
|
706
|
+
finally {
|
|
707
|
+
prompter?.close();
|
|
561
708
|
}
|
|
562
|
-
context.stdout.write('Next steps:\n');
|
|
563
|
-
context.stdout.write(` 1. Optional - preserve history: copy the .git directory from ${sourceRoot} into ${targetRoot}, then commit the migration on top (git rename detection preserves file history).\n`);
|
|
564
|
-
context.stdout.write(` 2. Execute the migration plan: ${migrationPlan.location}\n`);
|
|
565
|
-
context.stdout.write(' 3. Verify compliance: liftoff validate && liftoff doctor\n');
|
|
566
|
-
context.stdout.write(`The source project was not modified. Rolling back is deleting ${targetRoot}.\n`);
|
|
567
|
-
return 0;
|
|
568
709
|
}
|
|
569
710
|
async function migrateCommand(parsed, context) {
|
|
570
711
|
const sourceArg = parsed.positional[0];
|
|
571
712
|
if (!sourceArg) {
|
|
572
|
-
context.
|
|
713
|
+
context.presentation.error('Usage: liftoff migrate <path-to-existing-project>', 'Run `liftoff migrate --help` for accepted migration options.');
|
|
714
|
+
return 1;
|
|
715
|
+
}
|
|
716
|
+
const migrationOptions = await optionsFromParsedArgs(parsed, context.cwd, false);
|
|
717
|
+
if (migrationOptions.projectType === 'power-apps-code-app') {
|
|
718
|
+
context.presentation.error('Power Apps code app migration is not supported.', 'Initialize a fresh Power Apps code app with `liftoff init --type power-apps-code-app`, then move application changes intentionally.');
|
|
573
719
|
return 1;
|
|
574
720
|
}
|
|
575
721
|
const sourceRoot = path.resolve(context.cwd, sourceArg);
|
|
@@ -578,17 +724,18 @@ async function migrateCommand(parsed, context) {
|
|
|
578
724
|
sourceDetails = await stat(sourceRoot);
|
|
579
725
|
}
|
|
580
726
|
catch {
|
|
581
|
-
context.
|
|
727
|
+
context.presentation.error(`Source project not found: ${sourceRoot}`);
|
|
582
728
|
return 1;
|
|
583
729
|
}
|
|
584
730
|
if (!sourceDetails.isDirectory()) {
|
|
585
|
-
context.
|
|
731
|
+
context.presentation.error(`Source path is not a directory: ${sourceRoot}`);
|
|
586
732
|
return 1;
|
|
587
733
|
}
|
|
588
734
|
if (existsSync(path.join(sourceRoot, 'liftoff.manifest.json'))) {
|
|
589
|
-
context.
|
|
735
|
+
context.presentation.error(`${sourceRoot} is already a Liftoff project.`, 'Use `liftoff update` instead.');
|
|
590
736
|
return 1;
|
|
591
737
|
}
|
|
738
|
+
context.presentation.identity('Migrate an existing application into a fresh Liftoff project');
|
|
592
739
|
return withUnchangedMigrationSource(sourceRoot, () => executeMigration(parsed, context, sourceRoot));
|
|
593
740
|
}
|
|
594
741
|
function summarizeEntries(entries) {
|
|
@@ -659,47 +806,67 @@ async function preflightUpdate(projectRoot, entries, force) {
|
|
|
659
806
|
await resolveProjectPath(projectRoot, ['liftoff.manifest.json']);
|
|
660
807
|
}
|
|
661
808
|
async function updateCommand(parsed, context) {
|
|
809
|
+
const { presentation } = context;
|
|
662
810
|
const apply = readBooleanFlag(parsed.flags, 'apply') ?? false;
|
|
663
811
|
const force = readBooleanFlag(parsed.flags, 'force') ?? false;
|
|
664
812
|
const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
|
|
813
|
+
presentation.commandIdentity('update', 'Reconcile the project with current Liftoff templates');
|
|
665
814
|
if (force && !apply) {
|
|
666
|
-
|
|
815
|
+
presentation.error('--force requires --apply.', 'Run `liftoff update --apply --force` only after reviewing the reported conflicts.');
|
|
667
816
|
return 1;
|
|
668
817
|
}
|
|
669
818
|
const explicit = parsed.positional[0] ?? readStringFlag(parsed.flags, 'project');
|
|
670
819
|
const projectRoot = explicit ? path.resolve(context.cwd, explicit) : await findProjectRoot(context.cwd);
|
|
671
820
|
if (!projectRoot) {
|
|
672
|
-
|
|
821
|
+
presentation.error(`No liftoff.manifest.json found in ${context.cwd} or any parent directory.`, 'Run this command inside a Liftoff project or provide its path explicitly.');
|
|
673
822
|
return 1;
|
|
674
823
|
}
|
|
675
824
|
const manifest = await loadManifest(projectRoot);
|
|
676
825
|
if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
|
|
677
|
-
|
|
826
|
+
presentation.error(`This project was written by Liftoff ${manifest.liftoffVersion}, which is newer than this CLI (${liftoffVersion}).`, 'Upgrade the CLI first.');
|
|
678
827
|
return 1;
|
|
679
828
|
}
|
|
680
829
|
const config = await loadConfigOptions('liftoff.config.json', projectRoot);
|
|
681
830
|
const plan = buildProjectPlan(config, { requireProjectName: true });
|
|
682
|
-
|
|
683
|
-
|
|
831
|
+
const recordedWorkload = manifest.project.workload;
|
|
832
|
+
if (plan.workload !== recordedWorkload.kind) {
|
|
833
|
+
const involvesPowerApps = plan.workload === 'power-apps-code-app' ||
|
|
834
|
+
recordedWorkload.kind === 'power-apps-code-app';
|
|
835
|
+
presentation.error(`Project type changes (${recordedWorkload.kind} -> ${plan.workload}) are not supported by update.`, involvesPowerApps
|
|
836
|
+
? 'Initialize a fresh project for the new workload.'
|
|
837
|
+
: 'Run `liftoff migrate` instead.');
|
|
684
838
|
return 1;
|
|
685
839
|
}
|
|
686
|
-
if (plan.
|
|
687
|
-
|
|
688
|
-
|
|
840
|
+
if (plan.workload === 'power-apps-code-app' && recordedWorkload.kind === 'power-apps-code-app') {
|
|
841
|
+
const recordedStarter = recordedWorkload.starter;
|
|
842
|
+
if (plan.starter.repository !== recordedStarter.repository ||
|
|
843
|
+
plan.starter.path !== recordedStarter.path ||
|
|
844
|
+
plan.starter.commit !== recordedStarter.commit) {
|
|
845
|
+
presentation.error('The Power Apps starter repository, path, or commit differs from the recorded immutable source.', 'Restore the recorded source identity or initialize a fresh project.');
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
689
848
|
}
|
|
690
|
-
if (plan.
|
|
691
|
-
|
|
692
|
-
|
|
849
|
+
else if (plan.workload !== 'power-apps-code-app' && recordedWorkload.kind !== 'power-apps-code-app') {
|
|
850
|
+
if (plan.apiStack.id !== recordedWorkload.apiStack) {
|
|
851
|
+
presentation.error(`API stack changes (${recordedWorkload.apiStack} -> ${plan.apiStack.id}) are a migration, not an update.`, 'Run `liftoff migrate` instead.');
|
|
852
|
+
return 1;
|
|
853
|
+
}
|
|
854
|
+
const recordedPattern = recordedWorkload.kind === 'genai' ? recordedWorkload.pattern : undefined;
|
|
855
|
+
const desiredPattern = plan.workload === 'genai' ? plan.pattern.id : undefined;
|
|
856
|
+
if (desiredPattern !== recordedPattern) {
|
|
857
|
+
presentation.error(`Pattern changes (${recordedPattern ?? 'none'} -> ${desiredPattern ?? 'none'}) are a migration, not an update.`, 'Run `liftoff migrate` instead.');
|
|
858
|
+
return 1;
|
|
859
|
+
}
|
|
693
860
|
}
|
|
694
861
|
if (plan.specWorkflow.id !== manifest.project.specWorkflow) {
|
|
695
|
-
|
|
862
|
+
presentation.error(`Spec workflow changes (${manifest.project.specWorkflow} -> ${plan.specWorkflow.id}) require official framework initialization and are not supported by liftoff update.`, 'Restore the workflow recorded in liftoff.manifest.json or migrate into a fresh project.');
|
|
696
863
|
return 1;
|
|
697
864
|
}
|
|
698
865
|
const configuredAgents = plan.agents.map((agent) => agent.id);
|
|
699
866
|
if (manifest.framework.state === 'initialized' && (configuredAgents.length !== manifest.project.agents.length ||
|
|
700
867
|
configuredAgents.some((agent, index) => agent !== manifest.project.agents[index]) ||
|
|
701
868
|
plan.defaultAgent?.id !== manifest.project.defaultAgent)) {
|
|
702
|
-
|
|
869
|
+
presentation.error('AI agent or default-agent changes require official framework initialization and are not supported by liftoff update.', 'Restore liftoff.config.json to the integrations recorded in liftoff.manifest.json.');
|
|
703
870
|
return 1;
|
|
704
871
|
}
|
|
705
872
|
const render = buildArtifacts(plan);
|
|
@@ -709,7 +876,7 @@ async function updateCommand(parsed, context) {
|
|
|
709
876
|
const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
|
|
710
877
|
if (!apply) {
|
|
711
878
|
if (jsonMode) {
|
|
712
|
-
|
|
879
|
+
presentation.rawStdout(`${JSON.stringify({
|
|
713
880
|
schemaVersion: 1,
|
|
714
881
|
mode: 'check',
|
|
715
882
|
cliVersion: liftoffVersion,
|
|
@@ -725,39 +892,55 @@ async function updateCommand(parsed, context) {
|
|
|
725
892
|
}, null, 2)}\n`);
|
|
726
893
|
return drift ? 2 : 0;
|
|
727
894
|
}
|
|
728
|
-
|
|
895
|
+
presentation.definitions('Project versions', [
|
|
896
|
+
{ label: 'Liftoff CLI', value: liftoffVersion },
|
|
897
|
+
{ label: 'Project generated by', value: manifest.liftoffVersion }
|
|
898
|
+
]);
|
|
729
899
|
if (!drift) {
|
|
730
|
-
|
|
900
|
+
presentation.status('success', 'No drift', `${summary.unchanged} artifacts match the current templates and configuration`);
|
|
731
901
|
return 0;
|
|
732
902
|
}
|
|
733
|
-
|
|
734
|
-
|
|
903
|
+
if (presentation.stdout.layout === 'plain') {
|
|
904
|
+
presentation.section('Template drift', visible.map((entry) => `${entryMarker(entry)} ${entryDisplay(entry)} ${entry.reason}`));
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
presentation.table('Template drift', ['Change', 'Artifact', 'Reason'], visible.map((entry) => [entryMarker(entry), entryDisplay(entry), entry.reason]));
|
|
735
908
|
}
|
|
736
909
|
const toWrite = summary.new + summary.missing + summary.upgrade + summary.moved + summary.refresh;
|
|
737
|
-
|
|
738
|
-
|
|
910
|
+
presentation.status('warning', 'Drift detected', `${toWrite} to write, ${summary.conflict} conflict(s), ${summary.orphan} orphan(s), ${summary.unchanged} unchanged`);
|
|
911
|
+
presentation.command('liftoff update --apply');
|
|
739
912
|
return 2;
|
|
740
913
|
}
|
|
741
914
|
if (isDirtyGitWorktree(projectRoot)) {
|
|
742
|
-
|
|
915
|
+
presentation.warning('The project worktree has uncommitted changes; consider committing before applying.');
|
|
743
916
|
}
|
|
917
|
+
presentation.stage('Apply safe template changes', projectRoot);
|
|
744
918
|
await preflightUpdate(projectRoot, entries, force);
|
|
745
919
|
const written = [];
|
|
746
920
|
const skipped = [];
|
|
921
|
+
const mutations = [];
|
|
747
922
|
for (const entry of entries) {
|
|
748
923
|
switch (entry.status) {
|
|
749
924
|
case 'new':
|
|
750
925
|
case 'missing':
|
|
751
926
|
case 'upgrade':
|
|
752
|
-
|
|
927
|
+
mutations.push({
|
|
928
|
+
type: 'write',
|
|
929
|
+
pathParts: entry.pathParts,
|
|
930
|
+
content: entry.rendered.content
|
|
931
|
+
});
|
|
753
932
|
written.push(entry);
|
|
754
933
|
break;
|
|
755
934
|
case 'moved':
|
|
756
935
|
if (entry.cleanMove || force) {
|
|
757
936
|
if (!entry.destinationMatches) {
|
|
758
|
-
|
|
937
|
+
mutations.push({
|
|
938
|
+
type: 'write',
|
|
939
|
+
pathParts: entry.pathParts,
|
|
940
|
+
content: entry.rendered.content
|
|
941
|
+
});
|
|
759
942
|
}
|
|
760
|
-
|
|
943
|
+
mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
|
|
761
944
|
written.push(entry);
|
|
762
945
|
}
|
|
763
946
|
else {
|
|
@@ -766,9 +949,13 @@ async function updateCommand(parsed, context) {
|
|
|
766
949
|
break;
|
|
767
950
|
case 'conflict':
|
|
768
951
|
if (force) {
|
|
769
|
-
|
|
952
|
+
mutations.push({
|
|
953
|
+
type: 'write',
|
|
954
|
+
pathParts: entry.pathParts,
|
|
955
|
+
content: entry.rendered.content
|
|
956
|
+
});
|
|
770
957
|
if (entry.previousPathParts) {
|
|
771
|
-
|
|
958
|
+
mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
|
|
772
959
|
}
|
|
773
960
|
written.push(entry);
|
|
774
961
|
}
|
|
@@ -811,9 +998,14 @@ async function updateCommand(parsed, context) {
|
|
|
811
998
|
nextManifest.artifacts.push(oldByName.get(entry.logicalName));
|
|
812
999
|
}
|
|
813
1000
|
}
|
|
814
|
-
|
|
1001
|
+
mutations.push({
|
|
1002
|
+
type: 'write',
|
|
1003
|
+
pathParts: ['liftoff.manifest.json'],
|
|
1004
|
+
content: `${JSON.stringify(nextManifest, null, 2)}\n`
|
|
1005
|
+
});
|
|
1006
|
+
await applyProjectFileTransaction(projectRoot, mutations);
|
|
815
1007
|
if (jsonMode) {
|
|
816
|
-
|
|
1008
|
+
presentation.rawStdout(`${JSON.stringify({
|
|
817
1009
|
schemaVersion: 1,
|
|
818
1010
|
mode: 'apply',
|
|
819
1011
|
cliVersion: liftoffVersion,
|
|
@@ -824,18 +1016,17 @@ async function updateCommand(parsed, context) {
|
|
|
824
1016
|
}, null, 2)}\n`);
|
|
825
1017
|
return 0;
|
|
826
1018
|
}
|
|
827
|
-
|
|
828
|
-
|
|
1019
|
+
if (written.length > 0) {
|
|
1020
|
+
presentation.bullets('Applied changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
|
|
829
1021
|
}
|
|
830
|
-
|
|
831
|
-
|
|
1022
|
+
if (skipped.length > 0) {
|
|
1023
|
+
presentation.bullets('Skipped conflicts', skipped.map((entry) => `skipped ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (use --apply --force to overwrite)'}`));
|
|
832
1024
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
1025
|
+
const orphans = entries.filter((entry) => entry.status === 'orphan');
|
|
1026
|
+
if (orphans.length > 0) {
|
|
1027
|
+
presentation.bullets('Orphaned artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
|
|
837
1028
|
}
|
|
838
|
-
|
|
1029
|
+
presentation.completion('Updated project', `${written.length} written, ${skipped.length} skipped, ${summary.orphan} orphan(s)`, [{ label: 'Manifest version', value: liftoffVersion }], 'liftoff validate && liftoff doctor');
|
|
839
1030
|
return 0;
|
|
840
1031
|
}
|
|
841
1032
|
function versionedBinaryCheck(label, command, args, minimum, remedy) {
|
|
@@ -904,11 +1095,17 @@ function workstationLayer(probes) {
|
|
|
904
1095
|
}
|
|
905
1096
|
function workstationSelectionFromManifest(manifest) {
|
|
906
1097
|
const framework = getFrameworkDefinition(manifest.project.specWorkflow);
|
|
1098
|
+
const workload = manifest.project.workload;
|
|
907
1099
|
return {
|
|
908
|
-
|
|
1100
|
+
workload: workload.kind === 'power-apps-code-app'
|
|
1101
|
+
? { kind: 'power-apps-code-app' }
|
|
1102
|
+
: {
|
|
1103
|
+
kind: workload.kind,
|
|
1104
|
+
apiStack: { id: workload.apiStack },
|
|
1105
|
+
provider: { id: workload.cloud }
|
|
1106
|
+
},
|
|
909
1107
|
specWorkflow: { id: manifest.project.specWorkflow },
|
|
910
1108
|
framework: { version: framework.version },
|
|
911
|
-
provider: { id: manifest.project.cloud },
|
|
912
1109
|
agents: manifest.framework.state === 'initialized'
|
|
913
1110
|
? manifest.project.agents.map((id) => {
|
|
914
1111
|
const agent = getCodingAgent(id);
|
|
@@ -1014,6 +1211,26 @@ function stackProjectCheck(projectRoot, apiStack) {
|
|
|
1014
1211
|
remedy: `repair the generated ${apiStack} backend configuration`
|
|
1015
1212
|
};
|
|
1016
1213
|
}
|
|
1214
|
+
async function powerAppsProjectCheck(projectRoot) {
|
|
1215
|
+
const issues = await verifyPowerAppsPackageMetadata(projectRoot);
|
|
1216
|
+
if (issues.length === 0) {
|
|
1217
|
+
return {
|
|
1218
|
+
id: 'power-apps-project',
|
|
1219
|
+
label: 'Power Apps project',
|
|
1220
|
+
severity: 'ok',
|
|
1221
|
+
state: 'ready',
|
|
1222
|
+
detail: 'SDK, Vite plugin, and deterministic lockfile metadata are valid'
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
id: 'power-apps-project',
|
|
1227
|
+
label: 'Power Apps project',
|
|
1228
|
+
severity: 'fail',
|
|
1229
|
+
state: 'unhealthy',
|
|
1230
|
+
detail: issues[0],
|
|
1231
|
+
remedy: 'restore package.json and package-lock.json or run liftoff update --apply'
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1017
1234
|
function runReadOnly(command, args, cwd) {
|
|
1018
1235
|
const result = spawnSync(command, args, { cwd, encoding: 'utf8' });
|
|
1019
1236
|
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
@@ -1022,11 +1239,11 @@ function binaryPresent(command) {
|
|
|
1022
1239
|
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
1023
1240
|
return spawnSync(probe, [command], { encoding: 'utf8' }).status === 0;
|
|
1024
1241
|
}
|
|
1025
|
-
function azureCloudChecks() {
|
|
1026
|
-
|
|
1242
|
+
async function azureCloudChecks(runner) {
|
|
1243
|
+
const auth = await runner.run({ executable: 'az', args: ['account', 'show', '-o', 'none', '--only-show-errors'] }, { timeoutMs: 15_000 });
|
|
1244
|
+
if (auth.errorCode === 'ENOENT') {
|
|
1027
1245
|
return [{ label: 'az', severity: 'warn', detail: 'Azure CLI not found', remedy: 'install the Azure CLI' }];
|
|
1028
1246
|
}
|
|
1029
|
-
const auth = spawnSync('az', ['account', 'show', '-o', 'none', '--only-show-errors'], { encoding: 'utf8' });
|
|
1030
1247
|
if (auth.status === 0) {
|
|
1031
1248
|
return [{ label: 'azure auth', severity: 'ok', detail: 'authenticated' }];
|
|
1032
1249
|
}
|
|
@@ -1036,9 +1253,9 @@ function azureCloudChecks() {
|
|
|
1036
1253
|
const CLOUD_CHECKS = {
|
|
1037
1254
|
azure: azureCloudChecks
|
|
1038
1255
|
};
|
|
1039
|
-
function cloudLayer(cloud) {
|
|
1256
|
+
async function cloudLayer(cloud, runner) {
|
|
1040
1257
|
const checks = CLOUD_CHECKS[cloud]
|
|
1041
|
-
? CLOUD_CHECKS[cloud]()
|
|
1258
|
+
? await CLOUD_CHECKS[cloud](runner)
|
|
1042
1259
|
: [{ label: cloud, severity: 'skipped', detail: `${cloud} provider checks are not available yet` }];
|
|
1043
1260
|
return { title: `Cloud - ${cloud}`, checks };
|
|
1044
1261
|
}
|
|
@@ -1112,7 +1329,20 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1112
1329
|
checks.push({ label: 'version', severity: 'ok', detail: `generated by ${manifest.liftoffVersion}, CLI ${liftoffVersion}` });
|
|
1113
1330
|
}
|
|
1114
1331
|
checks.push(...await frameworkDoctorChecks(projectRoot, manifest));
|
|
1115
|
-
|
|
1332
|
+
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
1333
|
+
const starter = manifest.project.workload.starter;
|
|
1334
|
+
checks.push({
|
|
1335
|
+
id: 'power-apps-starter',
|
|
1336
|
+
label: 'Power Apps starter',
|
|
1337
|
+
severity: 'ok',
|
|
1338
|
+
state: 'ready',
|
|
1339
|
+
detail: `${starter.repository}/${starter.path} @ ${starter.commit}`
|
|
1340
|
+
});
|
|
1341
|
+
checks.push(await powerAppsProjectCheck(projectRoot));
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
checks.push(stackProjectCheck(projectRoot, manifest.project.workload.apiStack));
|
|
1345
|
+
}
|
|
1116
1346
|
try {
|
|
1117
1347
|
const config = await loadConfigOptions('liftoff.config.json', projectRoot);
|
|
1118
1348
|
const plan = buildProjectPlan(config, { requireProjectName: true });
|
|
@@ -1141,8 +1371,40 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1141
1371
|
}
|
|
1142
1372
|
return { title: 'Project', checks };
|
|
1143
1373
|
}
|
|
1144
|
-
async function runtimeLayer(projectRoot, dockerAvailable) {
|
|
1374
|
+
async function runtimeLayer(projectRoot, dockerAvailable, runner, manifest) {
|
|
1145
1375
|
const checks = [];
|
|
1376
|
+
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
1377
|
+
if (!existsSync(path.join(projectRoot, 'node_modules'))) {
|
|
1378
|
+
checks.push({
|
|
1379
|
+
id: 'power-apps-cli',
|
|
1380
|
+
label: 'Power Apps CLI',
|
|
1381
|
+
severity: 'skipped',
|
|
1382
|
+
state: 'not-observable',
|
|
1383
|
+
detail: 'project dependencies are not installed',
|
|
1384
|
+
remedy: 'run npm ci, then rerun liftoff doctor'
|
|
1385
|
+
});
|
|
1386
|
+
return { title: 'Runtime', checks };
|
|
1387
|
+
}
|
|
1388
|
+
const executable = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
1389
|
+
const result = await runner.run({ executable, args: ['--no-install', 'power-apps', '--version'] }, { cwd: projectRoot, timeoutMs: 15_000 });
|
|
1390
|
+
checks.push(result.status === 0
|
|
1391
|
+
? {
|
|
1392
|
+
id: 'power-apps-cli',
|
|
1393
|
+
label: 'Power Apps CLI',
|
|
1394
|
+
severity: 'ok',
|
|
1395
|
+
state: 'ready',
|
|
1396
|
+
detail: result.stdout.trim() || 'local CLI is available'
|
|
1397
|
+
}
|
|
1398
|
+
: {
|
|
1399
|
+
id: 'power-apps-cli',
|
|
1400
|
+
label: 'Power Apps CLI',
|
|
1401
|
+
severity: 'fail',
|
|
1402
|
+
state: 'unhealthy',
|
|
1403
|
+
detail: (result.stderr || 'local CLI probe failed').trim().split('\n')[0],
|
|
1404
|
+
remedy: 'run npm ci and verify @microsoft/power-apps before binding the environment'
|
|
1405
|
+
});
|
|
1406
|
+
return { title: 'Runtime', checks };
|
|
1407
|
+
}
|
|
1146
1408
|
if (existsSync(path.join(projectRoot, '.env.example'))) {
|
|
1147
1409
|
if (existsSync(path.join(projectRoot, '.env'))) {
|
|
1148
1410
|
checks.push({ label: '.env', severity: 'ok', detail: 'present' });
|
|
@@ -1161,7 +1423,7 @@ async function runtimeLayer(projectRoot, dockerAvailable) {
|
|
|
1161
1423
|
checks.push({ label: 'compose', severity: 'skipped', detail: 'docker is not installed, compose config not checked' });
|
|
1162
1424
|
}
|
|
1163
1425
|
else {
|
|
1164
|
-
const result =
|
|
1426
|
+
const result = await runner.run({ executable: 'docker', args: ['compose', 'config', '-q'] }, { cwd: projectRoot, timeoutMs: 15_000 });
|
|
1165
1427
|
if (result.status === 0) {
|
|
1166
1428
|
checks.push({ label: 'compose', severity: 'ok', detail: 'docker compose config is valid' });
|
|
1167
1429
|
}
|
|
@@ -1176,19 +1438,43 @@ async function runtimeLayer(projectRoot, dockerAvailable) {
|
|
|
1176
1438
|
}
|
|
1177
1439
|
return { title: 'Runtime', checks };
|
|
1178
1440
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1441
|
+
async function codeAppsPluginLayer(projectRoot, manifest, runner) {
|
|
1442
|
+
const agents = manifest.project.agents.map((id) => {
|
|
1443
|
+
const agent = getCodingAgent(id);
|
|
1444
|
+
if (!agent) {
|
|
1445
|
+
throw new Error(`Manifest references unknown coding agent ${id}.`);
|
|
1446
|
+
}
|
|
1447
|
+
return agent;
|
|
1448
|
+
});
|
|
1449
|
+
const probes = await probeCodeAppsPlugin(agents, runner, projectRoot);
|
|
1450
|
+
return {
|
|
1451
|
+
title: 'Optional Code Apps plugin',
|
|
1452
|
+
checks: probes.map((probe) => ({
|
|
1453
|
+
id: probe.id,
|
|
1454
|
+
label: probe.agent.label,
|
|
1455
|
+
severity: probe.state === 'ready' ? 'ok' : 'warn',
|
|
1456
|
+
state: probe.state,
|
|
1457
|
+
requirementSeverity: 'advisory',
|
|
1458
|
+
detail: probe.detail,
|
|
1459
|
+
...(probe.remedy ? { remedy: probe.remedy } : {})
|
|
1460
|
+
}))
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function renderDoctorLayers(layers, presentation) {
|
|
1464
|
+
const statusKind = {
|
|
1465
|
+
ok: 'success',
|
|
1466
|
+
warn: 'warning',
|
|
1467
|
+
fail: 'error',
|
|
1468
|
+
skipped: 'pending'
|
|
1469
|
+
};
|
|
1186
1470
|
for (const layer of layers) {
|
|
1187
|
-
|
|
1188
|
-
for (const check of layer.checks) {
|
|
1471
|
+
presentation.section(layer.title, layer.checks.flatMap((check) => {
|
|
1189
1472
|
const remedy = check.remedy ? ` - ${check.remedy}` : '';
|
|
1190
|
-
|
|
1191
|
-
|
|
1473
|
+
return presentation.stdout
|
|
1474
|
+
.status(statusKind[check.severity], check.label, `${check.detail}${remedy}`)
|
|
1475
|
+
.trimEnd()
|
|
1476
|
+
.split('\n');
|
|
1477
|
+
}));
|
|
1192
1478
|
}
|
|
1193
1479
|
}
|
|
1194
1480
|
export function doctorExitCode(layers) {
|
|
@@ -1198,6 +1484,7 @@ async function doctorCommand(parsed, context) {
|
|
|
1198
1484
|
const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
|
|
1199
1485
|
const cloudOverride = readStringFlag(parsed.flags, 'cloud');
|
|
1200
1486
|
const layers = [];
|
|
1487
|
+
context.presentation.commandIdentity('doctor', 'Inspect CLI, workstation, project, runtime, and cloud readiness');
|
|
1201
1488
|
const projectRoot = await findProjectRoot(context.cwd);
|
|
1202
1489
|
let manifest;
|
|
1203
1490
|
let manifestError;
|
|
@@ -1227,35 +1514,64 @@ async function doctorCommand(parsed, context) {
|
|
|
1227
1514
|
}
|
|
1228
1515
|
if (manifest) {
|
|
1229
1516
|
layers.push(await projectLayer(projectRoot, manifest));
|
|
1230
|
-
layers.push(await runtimeLayer(projectRoot, dockerAvailable));
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1517
|
+
layers.push(await runtimeLayer(projectRoot, dockerAvailable, runner, manifest));
|
|
1518
|
+
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
1519
|
+
if (manifest.project.workload.codeAppsPlugin) {
|
|
1520
|
+
layers.push(await codeAppsPluginLayer(projectRoot, manifest, runner));
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
const workload = manifest.project.workload;
|
|
1525
|
+
const cloud = cloudOverride ?? workload.cloud;
|
|
1526
|
+
const cloudChecks = await cloudLayer(cloud, runner);
|
|
1527
|
+
const pattern = workload.kind === 'genai'
|
|
1528
|
+
? patterns.find((candidate) => candidate.id === workload.pattern)
|
|
1529
|
+
: undefined;
|
|
1530
|
+
if (pattern?.worker && cloud === 'azure') {
|
|
1531
|
+
cloudChecks.checks.push(binaryPresent('func')
|
|
1532
|
+
? { label: 'functions tooling', severity: 'ok', detail: 'Azure Functions Core Tools installed' }
|
|
1533
|
+
: { label: 'functions tooling', severity: 'warn', detail: 'Azure Functions Core Tools not found', remedy: 'npm install -g azure-functions-core-tools@4' });
|
|
1534
|
+
}
|
|
1535
|
+
layers.push(cloudChecks);
|
|
1238
1536
|
}
|
|
1239
|
-
layers.push(cloudChecks);
|
|
1240
1537
|
}
|
|
1241
1538
|
}
|
|
1242
1539
|
else if (cloudOverride) {
|
|
1243
|
-
layers.push(cloudLayer(cloudOverride));
|
|
1540
|
+
layers.push(await cloudLayer(cloudOverride, runner));
|
|
1244
1541
|
}
|
|
1245
1542
|
const failures = layers.reduce((count, layer) => count + layer.checks.filter((check) => check.severity === 'fail').length, 0);
|
|
1246
1543
|
const warnings = layers.reduce((count, layer) => count + layer.checks.filter((check) => check.severity === 'warn').length, 0);
|
|
1247
1544
|
if (jsonMode) {
|
|
1248
|
-
context.
|
|
1545
|
+
context.presentation.rawStdout(`${JSON.stringify({ schemaVersion: 1, layers, summary: { failures, warnings } }, null, 2)}\n`);
|
|
1249
1546
|
}
|
|
1250
1547
|
else {
|
|
1251
|
-
renderDoctorLayers(layers, context.
|
|
1252
|
-
context.
|
|
1548
|
+
renderDoctorLayers(layers, context.presentation);
|
|
1549
|
+
context.presentation.status(failures > 0 ? 'error' : warnings > 0 ? 'warning' : 'success', 'Doctor summary', `${failures} failure(s), ${warnings} warning(s)`);
|
|
1253
1550
|
}
|
|
1254
1551
|
return doctorExitCode(layers);
|
|
1255
1552
|
}
|
|
1256
|
-
function helperCommand(parsed, context, tool) {
|
|
1553
|
+
async function helperCommand(parsed, context, tool) {
|
|
1554
|
+
const projectRoot = await findProjectRoot(context.cwd);
|
|
1555
|
+
if (projectRoot) {
|
|
1556
|
+
const manifest = await loadManifest(projectRoot);
|
|
1557
|
+
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
1558
|
+
if (parsed.command === 'dev') {
|
|
1559
|
+
context.presentation.commandIdentity('dev', 'Power Apps local development');
|
|
1560
|
+
context.presentation.section('Dependency prerequisite', ['Install the root lockfile before starting or after dependency changes.']);
|
|
1561
|
+
context.presentation.command('npm ci');
|
|
1562
|
+
context.presentation.section('Development server', []);
|
|
1563
|
+
context.presentation.command('npm run dev');
|
|
1564
|
+
return 0;
|
|
1565
|
+
}
|
|
1566
|
+
context.presentation.commandIdentity('infra', 'Power Apps hosting responsibility');
|
|
1567
|
+
context.presentation.status('info', 'Not applicable', 'Power Apps code apps are hosted by Power Platform; Liftoff-managed OpenTofu infrastructure is not generated.');
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1257
1571
|
const command = parsed.command === 'dev' ? buildDevCommand(parsed) : buildInfraCommand(parsed);
|
|
1258
|
-
context.
|
|
1572
|
+
context.presentation.commandIdentity(parsed.command ?? tool, `${tool} helper command`);
|
|
1573
|
+
context.presentation.section(`${tool} helper command`, []);
|
|
1574
|
+
context.presentation.command(command);
|
|
1259
1575
|
return 0;
|
|
1260
1576
|
}
|
|
1261
1577
|
function buildDevCommand(parsed) {
|
|
@@ -1292,9 +1608,8 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
|
|
|
1292
1608
|
const configOptions = configPath ? await loadConfigOptions(configPath, cwd) : {};
|
|
1293
1609
|
const flagOptions = {
|
|
1294
1610
|
projectName: includeProjectName ? parsed.positional[0] ?? readStringFlag(parsed.flags, 'project') : readStringFlag(parsed.flags, 'project'),
|
|
1295
|
-
projectType:
|
|
1296
|
-
|
|
1297
|
-
: readBooleanFlag(parsed.flags, 'genai') ? 'genai' : 'standard',
|
|
1611
|
+
projectType: readStringFlag(parsed.flags, 'type'),
|
|
1612
|
+
genai: readBooleanFlag(parsed.flags, 'genai'),
|
|
1298
1613
|
apiStack: readStringFlag(parsed.flags, 'api'),
|
|
1299
1614
|
pattern: readStringFlag(parsed.flags, 'pattern'),
|
|
1300
1615
|
cloud: readStringFlag(parsed.flags, 'cloud'),
|
|
@@ -1304,6 +1619,7 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
|
|
|
1304
1619
|
specWorkflow: readStringFlag(parsed.flags, 'spec'),
|
|
1305
1620
|
agents: readListFlag(parsed.flags, 'agents'),
|
|
1306
1621
|
defaultAgent: readStringFlag(parsed.flags, 'default-agent'),
|
|
1622
|
+
codeAppsPlugin: readBooleanFlag(parsed.flags, 'code-apps-plugin'),
|
|
1307
1623
|
configPath,
|
|
1308
1624
|
yes: readBooleanFlag(parsed.flags, 'yes') ?? false,
|
|
1309
1625
|
force: readBooleanFlag(parsed.flags, 'force'),
|
|
@@ -1313,24 +1629,56 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
|
|
|
1313
1629
|
return mergeOptions(configOptions, flagOptions);
|
|
1314
1630
|
}
|
|
1315
1631
|
function hasMissingInitInputs(options) {
|
|
1316
|
-
const projectType = options.projectType ??
|
|
1317
|
-
|
|
1632
|
+
const projectType = options.projectType ??
|
|
1633
|
+
(options.genai === undefined ? undefined : options.genai ? 'genai' : 'standard') ??
|
|
1634
|
+
(options.pattern ? 'genai' : options.apiStack ? 'standard' : undefined);
|
|
1635
|
+
const missingTypeSpecific = projectType === 'genai'
|
|
1636
|
+
? !options.pattern
|
|
1637
|
+
: projectType === 'standard'
|
|
1638
|
+
? !options.apiStack
|
|
1639
|
+
: projectType === 'power-apps-code-app'
|
|
1640
|
+
? options.codeAppsPlugin === undefined
|
|
1641
|
+
: true;
|
|
1318
1642
|
const missingDefaultAgent = options.specWorkflow === 'spec-kit' &&
|
|
1319
1643
|
(options.agents?.length ?? 0) > 1 &&
|
|
1320
1644
|
!options.defaultAgent;
|
|
1321
1645
|
return !options.projectName ||
|
|
1322
1646
|
missingTypeSpecific ||
|
|
1323
|
-
!options.cloud ||
|
|
1324
|
-
options.includeFrontend === undefined ||
|
|
1647
|
+
projectType !== 'power-apps-code-app' && !options.cloud ||
|
|
1648
|
+
projectType !== 'power-apps-code-app' && options.includeFrontend === undefined ||
|
|
1325
1649
|
!options.specWorkflow ||
|
|
1326
1650
|
!options.agents ||
|
|
1327
1651
|
missingDefaultAgent ||
|
|
1328
|
-
!options.environments;
|
|
1652
|
+
projectType !== 'power-apps-code-app' && !options.environments;
|
|
1329
1653
|
}
|
|
1330
|
-
function
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1654
|
+
function renderGeneralHelp(presentation) {
|
|
1655
|
+
const help = getGeneralHelp(liftoffVersion);
|
|
1656
|
+
presentation.identity(`${help.title} - ${help.subtitle}`);
|
|
1657
|
+
presentation.section('Usage', [help.usage]);
|
|
1658
|
+
presentation.table('Global options', ['Option', 'Description'], help.globalOptions.map((option) => [option.syntax, option.description]));
|
|
1659
|
+
for (const group of help.commandGroups) {
|
|
1660
|
+
presentation.table(group.title, ['Command', 'Description'], group.entries.map((entry) => [entry.syntax, entry.description]));
|
|
1661
|
+
}
|
|
1662
|
+
presentation.status('info', 'Tip', help.hint);
|
|
1663
|
+
}
|
|
1664
|
+
function renderCommandHelp(command, presentation) {
|
|
1665
|
+
const help = getCommandHelp(command);
|
|
1666
|
+
presentation.commandIdentity(help.command, help.description);
|
|
1667
|
+
presentation.section('Usage', [
|
|
1668
|
+
presentation.stdout.layout === 'plain' ? `Usage: ${help.usage}` : help.usage
|
|
1669
|
+
]);
|
|
1670
|
+
if (help.arguments.length > 0) {
|
|
1671
|
+
presentation.table('Arguments', ['Argument', 'Description'], help.arguments.map((argument) => [argument.syntax, argument.description]));
|
|
1672
|
+
}
|
|
1673
|
+
if (help.subcommands.length > 0) {
|
|
1674
|
+
presentation.bullets('Subcommands', help.subcommands);
|
|
1675
|
+
}
|
|
1676
|
+
for (const group of help.optionGroups) {
|
|
1677
|
+
presentation.table(group.title, ['Option', 'Description'], group.entries.map((entry) => [
|
|
1678
|
+
entry.syntax,
|
|
1679
|
+
`${entry.description}${entry.defaultValue ? ` (default: ${entry.defaultValue})` : ''}`
|
|
1680
|
+
]));
|
|
1681
|
+
}
|
|
1334
1682
|
}
|
|
1335
1683
|
export async function createFixtureProject(options) {
|
|
1336
1684
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'liftoff-'));
|