@msn-control/liftoff 0.4.1 → 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 (108) hide show
  1. package/README.md +65 -256
  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.js +7 -1
  52. package/dist/args.js.map +1 -1
  53. package/dist/catalogs.d.ts +3 -1
  54. package/dist/catalogs.js +23 -0
  55. package/dist/catalogs.js.map +1 -1
  56. package/dist/code-apps-plugin.d.ts +13 -0
  57. package/dist/code-apps-plugin.js +69 -0
  58. package/dist/code-apps-plugin.js.map +1 -0
  59. package/dist/commands.js +282 -55
  60. package/dist/commands.js.map +1 -1
  61. package/dist/file-system.d.ts +16 -1
  62. package/dist/file-system.js +320 -10
  63. package/dist/file-system.js.map +1 -1
  64. package/dist/genai-templates.d.ts +6 -6
  65. package/dist/genai-templates.js +0 -3
  66. package/dist/genai-templates.js.map +1 -1
  67. package/dist/interactive.d.ts +40 -3
  68. package/dist/interactive.js +180 -24
  69. package/dist/interactive.js.map +1 -1
  70. package/dist/migrate-plan.d.ts +4 -4
  71. package/dist/migrate-plan.js +1 -1
  72. package/dist/migrate-plan.js.map +1 -1
  73. package/dist/planner.js +177 -40
  74. package/dist/planner.js.map +1 -1
  75. package/dist/power-apps-assets.d.ts +30 -0
  76. package/dist/power-apps-assets.js +131 -0
  77. package/dist/power-apps-assets.js.map +1 -0
  78. package/dist/power-apps-templates.d.ts +3 -0
  79. package/dist/power-apps-templates.js +140 -0
  80. package/dist/power-apps-templates.js.map +1 -0
  81. package/dist/power-apps-validation.d.ts +1 -0
  82. package/dist/power-apps-validation.js +79 -0
  83. package/dist/power-apps-validation.js.map +1 -0
  84. package/dist/project-dependencies.d.ts +1 -0
  85. package/dist/project-dependencies.js +12 -1
  86. package/dist/project-dependencies.js.map +1 -1
  87. package/dist/standard-templates.d.ts +4 -4
  88. package/dist/standard-templates.js.map +1 -1
  89. package/dist/templates.d.ts +2 -2
  90. package/dist/templates.js +196 -50
  91. package/dist/templates.js.map +1 -1
  92. package/dist/types.d.ts +73 -15
  93. package/dist/workstation.d.ts +10 -5
  94. package/dist/workstation.js +25 -10
  95. package/dist/workstation.js.map +1 -1
  96. package/docs/assets/liftoff-terminal.svg +33 -0
  97. package/docs/azure-deployment.md +66 -0
  98. package/docs/cli-reference.md +128 -0
  99. package/docs/configuration-and-manifests.md +102 -0
  100. package/docs/existing-repositories.md +87 -0
  101. package/docs/getting-started.md +114 -0
  102. package/docs/prerequisites.md +99 -0
  103. package/docs/project-structure.md +119 -0
  104. package/docs/safety-and-consent.md +98 -0
  105. package/docs/spec-workflows-and-agents.md +92 -0
  106. package/docs/troubleshooting.md +133 -0
  107. package/docs/workloads.md +128 -0
  108. package/package.json +8 -2
package/dist/commands.js CHANGED
@@ -6,14 +6,15 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
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, assertNewOrEmptyDirectory, deleteProjectFile, findProjectRoot, loadManifest, manifestDisplayPath, resolveProjectPath, validateGeneratedProject, writeArtifacts, writeProjectFile } from './file-system.js';
12
- import { InteractivePrompter } from './interactive.js';
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
16
  import { buildProjectPlan, loadConfigOptions, mergeOptions, PlanValidationError, projectPlanEntries } from './planner.js';
16
- import { buildDependencySetupPlan, runDependencySetup } from './project-dependencies.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';
@@ -24,7 +25,9 @@ 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) {
26
27
  const helpRequested = parsed.command !== undefined && readBooleanFlag(parsed.flags, 'help') === true;
27
- const jsonMode = !helpRequested && (parsed.command === 'doctor' || parsed.command === 'update') &&
28
+ const jsonMode = !helpRequested && (parsed.command === 'doctor' ||
29
+ parsed.command === 'update' ||
30
+ parsed.command === 'validate') &&
28
31
  readBooleanFlag(parsed.flags, 'json') === true;
29
32
  const presentation = new PresentationSession({
30
33
  stdout: context.stdout,
@@ -80,6 +83,10 @@ export async function runCommand(parsed, context) {
80
83
  }
81
84
  }
82
85
  catch (error) {
86
+ if (error instanceof InteractiveCancelledError) {
87
+ presentation.cancellation('Interactive operation stopped.');
88
+ return 0;
89
+ }
83
90
  if (error instanceof PlanValidationError) {
84
91
  presentation.error(error.issues.join('\n'), parsed.command ? `Run \`liftoff ${parsed.command} --help\` to review accepted values.` : undefined);
85
92
  return 1;
@@ -103,7 +110,10 @@ async function initCommand(parsed, context) {
103
110
  ? new InteractivePrompter({
104
111
  input: context.stdin,
105
112
  output: context.stdout,
106
- presentation
113
+ presentation,
114
+ cwd: context.cwd,
115
+ configuredRoot: git.exact ? git.root : undefined,
116
+ runner
107
117
  })
108
118
  : undefined;
109
119
  try {
@@ -111,12 +121,24 @@ async function initCommand(parsed, context) {
111
121
  if (needsPrompts) {
112
122
  presentation.stage('Configure project');
113
123
  }
114
- const options = needsPrompts ? await prompter.promptForInitOptions(initial) : initial;
115
- const plan = buildProjectPlan(options, { requireProjectName: true });
116
- presentation.stage('Review resolved plan');
117
- const confirmed = options.yes === true
118
- ? (presentation.definitions('Resolved project plan', projectPlanEntries(plan)), true)
119
- : await prompter.confirmPlan(plan);
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
+ }
120
142
  if (!confirmed) {
121
143
  presentation.cancellation('Initialization stopped; no destination files were changed.');
122
144
  return 0;
@@ -175,7 +197,8 @@ async function initCommand(parsed, context) {
175
197
  }
176
198
  presentation.bullets('Configured integrations', [
177
199
  `${plan.specWorkflow.label} ${plan.framework.version}`,
178
- ...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`)
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}`)
179
202
  ]);
180
203
  if (readiness.deferred.length > 0) {
181
204
  presentation.bullets('Deferred advisory checks', readiness.deferred);
@@ -221,13 +244,22 @@ async function planCommand(parsed, context) {
221
244
  }
222
245
  async function ensureWorkstationReady(plan, options, context, runner, presentation, prompter, resumeInvocation = 'liftoff init', commandCwd) {
223
246
  const requirements = selectWorkstationRequirements(plan);
224
- let probes = await probeWorkstation(requirements, runner, { cwd: commandCwd });
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;
225
254
  presentation.table('Workstation readiness', ['Requirement', 'Level', 'State', 'Detail'], probes.map((probe) => [
226
255
  probe.requirement.definition.label,
227
256
  probe.requirement.severity,
228
257
  probe.state,
229
258
  probe.detail
230
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
+ }
231
263
  const actionable = probes.filter((probe) => probe.state !== 'ready');
232
264
  const host = await detectHostEnvironment();
233
265
  const installInstruction = (probe) => {
@@ -311,7 +343,7 @@ async function ensureWorkstationReady(plan, options, context, runner, presentati
311
343
  presentation.error('Workstation readiness is incomplete.', options.installTools
312
344
  ? `Open a new terminal if PATH changed, then rerun \`${resumeInvocation}\` with the same project options.`
313
345
  : `Resume with \`${resumeInvocation} --install-tools\` plus the same project options after reviewing the commands.`);
314
- return { ready: false, deferred: [], probes };
346
+ return { ready: false, deferred: [], probes, pluginProbes };
315
347
  }
316
348
  const deferred = [
317
349
  ...probes
@@ -319,9 +351,12 @@ async function ensureWorkstationReady(plan, options, context, runner, presentati
319
351
  .map((probe) => `${probe.requirement.definition.label}: ${probe.detail} Remedy: ${installInstruction(probe)}`),
320
352
  ...probes.flatMap((probe) => probe.notices
321
353
  .filter((notice) => notice.state !== 'ready')
322
- .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}` : ''}`)
323
358
  ];
324
- return { ready: true, deferred, probes };
359
+ return { ready: true, deferred, probes, pluginProbes };
325
360
  }
326
361
  async function handleProjectDependencies(plan, projectRoot, options, probes, context, runner, presentation, prompter) {
327
362
  const dependencyPlan = buildDependencySetupPlan(plan, projectRoot, probes);
@@ -334,9 +369,16 @@ async function handleProjectDependencies(plan, projectRoot, options, probes, con
334
369
  if (!installDependencies) {
335
370
  return {
336
371
  success: true,
337
- deferred: dependencyPlan.commands.map((command) => `${command.label}: ${command.cwd} -> ${formatCommand(command.command)}`)
372
+ deferred: dependencyPlan.commands.map((command) => `${command.label}: ${dependencyResumeCommand(command)}`)
338
373
  };
339
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
+ }
340
382
  presentation.stage('Install project dependencies');
341
383
  const result = await runDependencySetup(dependencyPlan, projectRoot, runner, {
342
384
  ...presentation.childStreams(),
@@ -387,11 +429,21 @@ function regionsCommand(parsed, context) {
387
429
  }
388
430
  async function validateCommand(parsed, context) {
389
431
  context.presentation.commandIdentity('validate', 'Validate a generated Liftoff project');
432
+ const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
390
433
  const explicit = parsed.positional[0] ?? readStringFlag(parsed.flags, 'project');
391
434
  const projectRoot = explicit
392
435
  ? path.resolve(context.cwd, explicit)
393
436
  : (await findProjectRoot(context.cwd)) ?? context.cwd;
394
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
+ }
395
447
  if (issues.length > 0) {
396
448
  context.presentation.error(issues.join('\n'), 'Restore invalid generated files or the manifest from version control, then rerun validation.');
397
449
  return 1;
@@ -534,12 +586,15 @@ async function executeMigration(parsed, context, sourceRoot) {
534
586
  if (flagOptions.projectType === 'genai' && flagOptions.apiStack === undefined) {
535
587
  initial.apiStack = undefined;
536
588
  }
589
+ const runner = context.runner ?? new NodeCommandRunner();
537
590
  const interactive = initial.yes !== true;
538
591
  const prompter = interactive
539
592
  ? new InteractivePrompter({
540
593
  input: context.stdin,
541
594
  output: context.stdout,
542
- presentation
595
+ presentation,
596
+ cwd: context.cwd,
597
+ runner
543
598
  })
544
599
  : undefined;
545
600
  try {
@@ -549,6 +604,10 @@ async function executeMigration(parsed, context, sourceRoot) {
549
604
  }
550
605
  const options = needsPrompts ? await prompter.promptForInitOptions(initial) : initial;
551
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;
610
+ }
552
611
  presentation.stage('Review migration plan');
553
612
  const confirmed = options.yes === true
554
613
  ? (presentation.definitions('Resolved migration plan', projectPlanEntries(plan)), true)
@@ -566,7 +625,6 @@ async function executeMigration(parsed, context, sourceRoot) {
566
625
  const target = { root: targetRoot, mode: 'named-child' };
567
626
  await assertSafeInitTarget(target, parentDir);
568
627
  await assertNewOrEmptyDirectory(targetRoot);
569
- const runner = context.runner ?? new NodeCommandRunner();
570
628
  presentation.stage('Check workstation readiness');
571
629
  const readinessRoot = await mkdtemp(path.join(os.tmpdir(), 'liftoff-migrate-readiness-'));
572
630
  const readiness = await (async () => {
@@ -655,6 +713,11 @@ async function migrateCommand(parsed, context) {
655
713
  context.presentation.error('Usage: liftoff migrate <path-to-existing-project>', 'Run `liftoff migrate --help` for accepted migration options.');
656
714
  return 1;
657
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.');
719
+ return 1;
720
+ }
658
721
  const sourceRoot = path.resolve(context.cwd, sourceArg);
659
722
  let sourceDetails;
660
723
  try {
@@ -765,17 +828,35 @@ async function updateCommand(parsed, context) {
765
828
  }
766
829
  const config = await loadConfigOptions('liftoff.config.json', projectRoot);
767
830
  const plan = buildProjectPlan(config, { requireProjectName: true });
768
- if (plan.projectType.id !== manifest.project.projectType) {
769
- presentation.error(`Project type changes (${manifest.project.projectType} -> ${plan.projectType.id}) are a migration, not an update.`, 'Run `liftoff migrate` instead.');
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.');
770
838
  return 1;
771
839
  }
772
- if (plan.apiStack.id !== manifest.project.apiStack) {
773
- presentation.error(`API stack changes (${manifest.project.apiStack} -> ${plan.apiStack.id}) are a migration, not an update.`, 'Run `liftoff migrate` instead.');
774
- return 1;
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
+ }
775
848
  }
776
- if (plan.pattern?.id !== manifest.project.pattern) {
777
- presentation.error(`Pattern changes (${manifest.project.pattern ?? 'none'} -> ${plan.pattern?.id ?? 'none'}) are a migration, not an update.`, 'Run `liftoff migrate` instead.');
778
- return 1;
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
+ }
779
860
  }
780
861
  if (plan.specWorkflow.id !== manifest.project.specWorkflow) {
781
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.');
@@ -837,20 +918,29 @@ async function updateCommand(parsed, context) {
837
918
  await preflightUpdate(projectRoot, entries, force);
838
919
  const written = [];
839
920
  const skipped = [];
921
+ const mutations = [];
840
922
  for (const entry of entries) {
841
923
  switch (entry.status) {
842
924
  case 'new':
843
925
  case 'missing':
844
926
  case 'upgrade':
845
- await writeProjectFile(projectRoot, entry.pathParts, entry.rendered.content);
927
+ mutations.push({
928
+ type: 'write',
929
+ pathParts: entry.pathParts,
930
+ content: entry.rendered.content
931
+ });
846
932
  written.push(entry);
847
933
  break;
848
934
  case 'moved':
849
935
  if (entry.cleanMove || force) {
850
936
  if (!entry.destinationMatches) {
851
- await writeProjectFile(projectRoot, entry.pathParts, entry.rendered.content);
937
+ mutations.push({
938
+ type: 'write',
939
+ pathParts: entry.pathParts,
940
+ content: entry.rendered.content
941
+ });
852
942
  }
853
- await deleteProjectFile(projectRoot, entry.previousPathParts);
943
+ mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
854
944
  written.push(entry);
855
945
  }
856
946
  else {
@@ -859,9 +949,13 @@ async function updateCommand(parsed, context) {
859
949
  break;
860
950
  case 'conflict':
861
951
  if (force) {
862
- await writeProjectFile(projectRoot, entry.pathParts, entry.rendered.content);
952
+ mutations.push({
953
+ type: 'write',
954
+ pathParts: entry.pathParts,
955
+ content: entry.rendered.content
956
+ });
863
957
  if (entry.previousPathParts) {
864
- await deleteProjectFile(projectRoot, entry.previousPathParts);
958
+ mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
865
959
  }
866
960
  written.push(entry);
867
961
  }
@@ -904,7 +998,12 @@ async function updateCommand(parsed, context) {
904
998
  nextManifest.artifacts.push(oldByName.get(entry.logicalName));
905
999
  }
906
1000
  }
907
- await writeProjectFile(projectRoot, ['liftoff.manifest.json'], `${JSON.stringify(nextManifest, null, 2)}\n`);
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);
908
1007
  if (jsonMode) {
909
1008
  presentation.rawStdout(`${JSON.stringify({
910
1009
  schemaVersion: 1,
@@ -996,11 +1095,17 @@ function workstationLayer(probes) {
996
1095
  }
997
1096
  function workstationSelectionFromManifest(manifest) {
998
1097
  const framework = getFrameworkDefinition(manifest.project.specWorkflow);
1098
+ const workload = manifest.project.workload;
999
1099
  return {
1000
- apiStack: { id: manifest.project.apiStack },
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
+ },
1001
1107
  specWorkflow: { id: manifest.project.specWorkflow },
1002
1108
  framework: { version: framework.version },
1003
- provider: { id: manifest.project.cloud },
1004
1109
  agents: manifest.framework.state === 'initialized'
1005
1110
  ? manifest.project.agents.map((id) => {
1006
1111
  const agent = getCodingAgent(id);
@@ -1106,6 +1211,26 @@ function stackProjectCheck(projectRoot, apiStack) {
1106
1211
  remedy: `repair the generated ${apiStack} backend configuration`
1107
1212
  };
1108
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
+ }
1109
1234
  function runReadOnly(command, args, cwd) {
1110
1235
  const result = spawnSync(command, args, { cwd, encoding: 'utf8' });
1111
1236
  return { status: result.status, stdout: result.stdout, stderr: result.stderr };
@@ -1204,7 +1329,20 @@ async function projectLayer(projectRoot, manifest) {
1204
1329
  checks.push({ label: 'version', severity: 'ok', detail: `generated by ${manifest.liftoffVersion}, CLI ${liftoffVersion}` });
1205
1330
  }
1206
1331
  checks.push(...await frameworkDoctorChecks(projectRoot, manifest));
1207
- checks.push(stackProjectCheck(projectRoot, manifest.project.apiStack));
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
+ }
1208
1346
  try {
1209
1347
  const config = await loadConfigOptions('liftoff.config.json', projectRoot);
1210
1348
  const plan = buildProjectPlan(config, { requireProjectName: true });
@@ -1233,8 +1371,40 @@ async function projectLayer(projectRoot, manifest) {
1233
1371
  }
1234
1372
  return { title: 'Project', checks };
1235
1373
  }
1236
- async function runtimeLayer(projectRoot, dockerAvailable, runner) {
1374
+ async function runtimeLayer(projectRoot, dockerAvailable, runner, manifest) {
1237
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
+ }
1238
1408
  if (existsSync(path.join(projectRoot, '.env.example'))) {
1239
1409
  if (existsSync(path.join(projectRoot, '.env'))) {
1240
1410
  checks.push({ label: '.env', severity: 'ok', detail: 'present' });
@@ -1268,6 +1438,28 @@ async function runtimeLayer(projectRoot, dockerAvailable, runner) {
1268
1438
  }
1269
1439
  return { title: 'Runtime', checks };
1270
1440
  }
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
+ }
1271
1463
  function renderDoctorLayers(layers, presentation) {
1272
1464
  const statusKind = {
1273
1465
  ok: 'success',
@@ -1322,16 +1514,26 @@ async function doctorCommand(parsed, context) {
1322
1514
  }
1323
1515
  if (manifest) {
1324
1516
  layers.push(await projectLayer(projectRoot, manifest));
1325
- layers.push(await runtimeLayer(projectRoot, dockerAvailable, runner));
1326
- const cloud = cloudOverride ?? manifest.project.cloud;
1327
- const cloudChecks = await cloudLayer(cloud, runner);
1328
- const pattern = patterns.find((candidate) => candidate.id === manifest.project.pattern);
1329
- if (pattern?.worker && cloud === 'azure') {
1330
- cloudChecks.checks.push(binaryPresent('func')
1331
- ? { label: 'functions tooling', severity: 'ok', detail: 'Azure Functions Core Tools installed' }
1332
- : { label: 'functions tooling', severity: 'warn', detail: 'Azure Functions Core Tools not found', remedy: 'npm install -g azure-functions-core-tools@4' });
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);
1333
1536
  }
1334
- layers.push(cloudChecks);
1335
1537
  }
1336
1538
  }
1337
1539
  else if (cloudOverride) {
@@ -1348,7 +1550,24 @@ async function doctorCommand(parsed, context) {
1348
1550
  }
1349
1551
  return doctorExitCode(layers);
1350
1552
  }
1351
- 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
+ }
1352
1571
  const command = parsed.command === 'dev' ? buildDevCommand(parsed) : buildInfraCommand(parsed);
1353
1572
  context.presentation.commandIdentity(parsed.command ?? tool, `${tool} helper command`);
1354
1573
  context.presentation.section(`${tool} helper command`, []);
@@ -1389,9 +1608,8 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
1389
1608
  const configOptions = configPath ? await loadConfigOptions(configPath, cwd) : {};
1390
1609
  const flagOptions = {
1391
1610
  projectName: includeProjectName ? parsed.positional[0] ?? readStringFlag(parsed.flags, 'project') : readStringFlag(parsed.flags, 'project'),
1392
- projectType: readBooleanFlag(parsed.flags, 'genai') === undefined
1393
- ? undefined
1394
- : readBooleanFlag(parsed.flags, 'genai') ? 'genai' : 'standard',
1611
+ projectType: readStringFlag(parsed.flags, 'type'),
1612
+ genai: readBooleanFlag(parsed.flags, 'genai'),
1395
1613
  apiStack: readStringFlag(parsed.flags, 'api'),
1396
1614
  pattern: readStringFlag(parsed.flags, 'pattern'),
1397
1615
  cloud: readStringFlag(parsed.flags, 'cloud'),
@@ -1401,6 +1619,7 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
1401
1619
  specWorkflow: readStringFlag(parsed.flags, 'spec'),
1402
1620
  agents: readListFlag(parsed.flags, 'agents'),
1403
1621
  defaultAgent: readStringFlag(parsed.flags, 'default-agent'),
1622
+ codeAppsPlugin: readBooleanFlag(parsed.flags, 'code-apps-plugin'),
1404
1623
  configPath,
1405
1624
  yes: readBooleanFlag(parsed.flags, 'yes') ?? false,
1406
1625
  force: readBooleanFlag(parsed.flags, 'force'),
@@ -1410,19 +1629,27 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
1410
1629
  return mergeOptions(configOptions, flagOptions);
1411
1630
  }
1412
1631
  function hasMissingInitInputs(options) {
1413
- const projectType = options.projectType ?? (options.pattern ? 'genai' : options.apiStack ? 'standard' : undefined);
1414
- const missingTypeSpecific = projectType === 'genai' ? !options.pattern : projectType === 'standard' ? !options.apiStack : true;
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;
1415
1642
  const missingDefaultAgent = options.specWorkflow === 'spec-kit' &&
1416
1643
  (options.agents?.length ?? 0) > 1 &&
1417
1644
  !options.defaultAgent;
1418
1645
  return !options.projectName ||
1419
1646
  missingTypeSpecific ||
1420
- !options.cloud ||
1421
- options.includeFrontend === undefined ||
1647
+ projectType !== 'power-apps-code-app' && !options.cloud ||
1648
+ projectType !== 'power-apps-code-app' && options.includeFrontend === undefined ||
1422
1649
  !options.specWorkflow ||
1423
1650
  !options.agents ||
1424
1651
  missingDefaultAgent ||
1425
- !options.environments;
1652
+ projectType !== 'power-apps-code-app' && !options.environments;
1426
1653
  }
1427
1654
  function renderGeneralHelp(presentation) {
1428
1655
  const help = getGeneralHelp(liftoffVersion);