create-powerapp 1.0.1 → 1.1.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/.github/workflows/ci.yml +108 -112
- package/index.js +868 -964
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,964 +1,868 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* create-powerapp
|
|
5
|
-
* One command to scaffold any Power Apps project — Canvas App, Model-Driven App,
|
|
6
|
-
* PCF Component, Power Pages, or Code App — with Git, AI context files, and
|
|
7
|
-
* everything ready so you can immediately start prompting your AI assistant.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { execSync, spawnSync } from "child_process";
|
|
11
|
-
import fs from "fs";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import readline from "readline";
|
|
14
|
-
import { fileURLToPath } from "url";
|
|
15
|
-
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
|
|
18
|
-
// ─── Colour helpers ──────────────────────────────────────────────────────────
|
|
19
|
-
const c = {
|
|
20
|
-
reset: "\x1b[0m",
|
|
21
|
-
bold: "\x1b[1m",
|
|
22
|
-
green: "\x1b[32m",
|
|
23
|
-
cyan: "\x1b[36m",
|
|
24
|
-
yellow: "\x1b[33m",
|
|
25
|
-
red: "\x1b[31m",
|
|
26
|
-
dim: "\x1b[2m",
|
|
27
|
-
magenta: "\x1b[35m",
|
|
28
|
-
};
|
|
29
|
-
const log = {
|
|
30
|
-
info: (m) => console.log(`${c.cyan}ℹ${c.reset} ${m}`),
|
|
31
|
-
ok: (m) => console.log(`${c.green}✔${c.reset} ${m}`),
|
|
32
|
-
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
33
|
-
error: (m) => console.log(`${c.red}✖${c.reset} ${m}`),
|
|
34
|
-
step: (m) => console.log(`\n${c.bold}${c.magenta}▶ ${m}${c.reset}`),
|
|
35
|
-
dim: (m) => console.log(`${c.dim} ${m}${c.reset}`),
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// ─── readline helper ─────────────────────────────────────────────────────────
|
|
39
|
-
// Single persistent readline interface — works interactively AND when piped (CI)
|
|
40
|
-
const rl = readline.createInterface({
|
|
41
|
-
input: process.stdin,
|
|
42
|
-
output: process.stdout,
|
|
43
|
-
terminal: false, // don't assume TTY — makes piped input work in CI
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Queue of lines already read from stdin (for piped / non-interactive mode)
|
|
47
|
-
const lineQueue = [];
|
|
48
|
-
const lineWaiters = [];
|
|
49
|
-
rl.on("line", (line) => {
|
|
50
|
-
if (lineWaiters.length > 0) {
|
|
51
|
-
lineWaiters.shift()(line.trim());
|
|
52
|
-
} else {
|
|
53
|
-
lineQueue.push(line.trim());
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
function prompt(question) {
|
|
58
|
-
process.stdout.write(question);
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
if (lineQueue.length > 0) {
|
|
61
|
-
const line = lineQueue.shift();
|
|
62
|
-
process.stdout.write(line + "\n"); // echo the pre-buffered answer
|
|
63
|
-
resolve(line);
|
|
64
|
-
} else {
|
|
65
|
-
lineWaiters.push(resolve);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function choose(question, options) {
|
|
71
|
-
console.log(`\n${c.bold}${question}${c.reset}`);
|
|
72
|
-
options.forEach((o, i) => console.log(` ${c.cyan}${i + 1}${c.reset}. ${o.label} ${c.dim}${o.desc || ""}${c.reset}`));
|
|
73
|
-
while (true) {
|
|
74
|
-
const answer = await prompt(`\nEnter number (1–${options.length}): `);
|
|
75
|
-
const idx = parseInt(answer, 10) - 1;
|
|
76
|
-
if (idx >= 0 && idx < options.length) return options[idx];
|
|
77
|
-
log.warn("Please enter a valid number.");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── run shell command ────────────────────────────────────────────────────────
|
|
82
|
-
function run(cmd, cwd = process.cwd(), silent = false) {
|
|
83
|
-
const result = spawnSync(cmd, { shell: true, cwd, stdio: silent ? "pipe" : "inherit" });
|
|
84
|
-
if (result.status !== 0 && !silent) {
|
|
85
|
-
log.error(`Command failed: ${cmd}`);
|
|
86
|
-
}
|
|
87
|
-
return result.status === 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ─── check a CLI tool exists ──────────────────────────────────────────────────
|
|
91
|
-
function has(tool) {
|
|
92
|
-
return spawnSync(`${tool} --version`, { shell: true, stdio: "pipe" }).status === 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ─── write file helper ────────────────────────────────────────────────────────
|
|
96
|
-
function write(filePath, content) {
|
|
97
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
98
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ─── shared file content ──────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
function gitignoreContent() {
|
|
104
|
-
return `# Dependencies
|
|
105
|
-
node_modules/
|
|
106
|
-
.pnp
|
|
107
|
-
.pnp.js
|
|
108
|
-
|
|
109
|
-
# Environment variables — NEVER commit these
|
|
110
|
-
.env
|
|
111
|
-
.env.local
|
|
112
|
-
.env.*.local
|
|
113
|
-
|
|
114
|
-
# Build outputs
|
|
115
|
-
dist/
|
|
116
|
-
build/
|
|
117
|
-
out/
|
|
118
|
-
.next/
|
|
119
|
-
bin/
|
|
120
|
-
|
|
121
|
-
# Editor files
|
|
122
|
-
.vscode/
|
|
123
|
-
.idea/
|
|
124
|
-
*.swp
|
|
125
|
-
|
|
126
|
-
# OS files
|
|
127
|
-
.DS_Store
|
|
128
|
-
Thumbs.db
|
|
129
|
-
|
|
130
|
-
# Logs
|
|
131
|
-
*.log
|
|
132
|
-
npm-debug.log*
|
|
133
|
-
|
|
134
|
-
# Power Apps / PAC CLI outputs
|
|
135
|
-
*.msapp
|
|
136
|
-
*.zip
|
|
137
|
-
/obj/
|
|
138
|
-
`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function envContent(projectName, appType) {
|
|
142
|
-
return `# .env — DO NOT commit this file to Git
|
|
143
|
-
# Fill in the values for your project
|
|
144
|
-
|
|
145
|
-
# Your Power Platform environment URL
|
|
146
|
-
# Find it at: make.powerapps.com → Settings gear → Session details → Instance url
|
|
147
|
-
POWER_PLATFORM_URL=https://yourorg.crm.dynamics.com
|
|
148
|
-
|
|
149
|
-
# Project info
|
|
150
|
-
PROJECT_NAME=${projectName}
|
|
151
|
-
APP_TYPE=${appType}
|
|
152
|
-
|
|
153
|
-
# Add any other secrets here (API keys, database URLs, etc.)
|
|
154
|
-
# API_KEY=your-key-here
|
|
155
|
-
`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function readmeContent(projectName, appType, aiTool) {
|
|
159
|
-
return `# ${projectName}
|
|
160
|
-
|
|
161
|
-
**Type:** ${appType}
|
|
162
|
-
**AI Assistant:** ${aiTool}
|
|
163
|
-
|
|
164
|
-
## Getting started
|
|
165
|
-
|
|
166
|
-
Everything is already set up. Just open your AI assistant and describe what you want to build.
|
|
167
|
-
|
|
168
|
-
### Start your AI session
|
|
169
|
-
|
|
170
|
-
${aiTool === "Claude Code"
|
|
171
|
-
? "```bash\nclaude\n```\nThen type what you want to build in plain English."
|
|
172
|
-
: "Click the **Copilot Chat** icon (💬) in the VS Code sidebar and type what you want to build."}
|
|
173
|
-
|
|
174
|
-
### Example first prompts
|
|
175
|
-
|
|
176
|
-
> "Create the main screen for this app with a navigation menu and a welcome message."
|
|
177
|
-
|
|
178
|
-
> "Add a form that lets users submit [whatever your app needs] and saves it to Dataverse."
|
|
179
|
-
|
|
180
|
-
> "Explain what files are in this project and what each one does."
|
|
181
|
-
|
|
182
|
-
## Project structure
|
|
183
|
-
|
|
184
|
-
See the \`instructions/\` folder for the AI context files that tell your AI assistant
|
|
185
|
-
how this type of Power App works.
|
|
186
|
-
|
|
187
|
-
## Useful commands
|
|
188
|
-
|
|
189
|
-
\`\`\`bash
|
|
190
|
-
# Push changes to Power Platform
|
|
191
|
-
${appType === "Canvas App" ? "pac canvas pack --msapp ./CanvasApp.msapp --sources ./canvas-src\npac solution push" : ""}
|
|
192
|
-
${appType === "Model-Driven App" ? "pac solution push" : ""}
|
|
193
|
-
${appType === "PCF Component" ? "npm run build\npac pcf push --publisher-prefix yourprefix" : ""}
|
|
194
|
-
${appType === "Power Pages" ? "pac pages upload --path ." : ""}
|
|
195
|
-
${appType === "Code App" ? "npm run build\npac code push" : ""}
|
|
196
|
-
|
|
197
|
-
# Save your work
|
|
198
|
-
git add .
|
|
199
|
-
git commit -m "describe what you changed"
|
|
200
|
-
git push
|
|
201
|
-
\`\`\`
|
|
202
|
-
`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ─── App-type scaffolders ─────────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
function scaffoldCanvas(dir, projectName, aiTool) {
|
|
208
|
-
log.info("Setting up Canvas App structure...");
|
|
209
|
-
|
|
210
|
-
// AI context files
|
|
211
|
-
write(path.join(dir, "instructions", "canvas-instructions.md"), `# Canvas App Instructions
|
|
212
|
-
|
|
213
|
-
## Type: Canvas App (Power Apps)
|
|
214
|
-
## Project: ${projectName}
|
|
215
|
-
|
|
216
|
-
- Screens and controls are defined in .fx.yaml files
|
|
217
|
-
- Formulas use Power Fx — similar to Excel syntax
|
|
218
|
-
- Use With(), Collect(), Patch() for data operations — never SQL
|
|
219
|
-
- Navigation: Navigate(ScreenName, ScreenTransition.Fade)
|
|
220
|
-
- Avoid nested galleries — use collections instead
|
|
221
|
-
- PAC commands: pac canvas unpack / pac canvas pack
|
|
222
|
-
- Never edit .msapp binary directly — always unpack first
|
|
223
|
-
- Assets (images, icons) go in Assets/
|
|
224
|
-
- Data source connections go in DataSources/
|
|
225
|
-
`);
|
|
226
|
-
|
|
227
|
-
write(path.join(dir, "canvas-src", "Src", ".gitkeep"), "");
|
|
228
|
-
write(path.join(dir, "canvas-src", "Assets", ".gitkeep"), "");
|
|
229
|
-
write(path.join(dir, "canvas-src", "DataSources", ".gitkeep"), "");
|
|
230
|
-
write(path.join(dir, "canvas-src", "pkgs", ".gitkeep"), "");
|
|
231
|
-
|
|
232
|
-
write(path.join(dir, "canvas-src", "Src", "App.fx.yaml"), `# App-level settings and global variables
|
|
233
|
-
# The AI will help you fill this in as you build
|
|
234
|
-
`);
|
|
235
|
-
|
|
236
|
-
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
237
|
-
|
|
238
|
-
Copy and paste these into your AI assistant to get started quickly.
|
|
239
|
-
|
|
240
|
-
## Create your first screen
|
|
241
|
-
"I'm building a Canvas App called ${projectName}. Create the Home screen with:
|
|
242
|
-
- A header showing the app title and the current user's name
|
|
243
|
-
- A navigation menu with buttons for the main sections of the app
|
|
244
|
-
- A clean, professional layout using neutral colours"
|
|
245
|
-
|
|
246
|
-
## Add a data form
|
|
247
|
-
"Add a new screen with a form that lets users enter [describe your data].
|
|
248
|
-
Save it to a Dataverse table called [TableName]. Include validation so required fields can't be empty."
|
|
249
|
-
|
|
250
|
-
## Connect to data
|
|
251
|
-
"Connect this app to a SharePoint list called [ListName] at [SharePoint URL].
|
|
252
|
-
Show the items in a gallery on the Home screen, sorted by most recent first."
|
|
253
|
-
`);
|
|
254
|
-
|
|
255
|
-
log.ok("Canvas App structure created");
|
|
256
|
-
log.dim("Next: run pac canvas unpack --msapp YourApp.msapp --sources ./canvas-src to import an existing app");
|
|
257
|
-
log.dim("Or ask your AI: 'Create the first screen for a Canvas App called " + projectName + "'");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function scaffoldMDA(dir, projectName, aiTool) {
|
|
261
|
-
log.info("Setting up Model-Driven App structure...");
|
|
262
|
-
|
|
263
|
-
write(path.join(dir, "instructions", "mda-instructions.md"), `# Model-Driven App Instructions
|
|
264
|
-
|
|
265
|
-
## Type: Model-Driven App (Power Apps / Dataverse)
|
|
266
|
-
## Project: ${projectName}
|
|
267
|
-
|
|
268
|
-
- All entities map to Dataverse tables
|
|
269
|
-
- Forms are defined in XML — do not hand-edit GUIDs
|
|
270
|
-
- Business rules go in Dataverse, not client-side JS
|
|
271
|
-
- Use PAC CLI to push/pull: pac solution push / pac solution clone
|
|
272
|
-
- Plugin code goes in the Plugins/ folder, registered via Plugin Registration Tool
|
|
273
|
-
- JavaScript web resources live in WebResources/ and must be registered on forms
|
|
274
|
-
- SiteMaps define the app navigation structure
|
|
275
|
-
`);
|
|
276
|
-
|
|
277
|
-
write(path.join(dir, "solutions", ".gitkeep"), "");
|
|
278
|
-
|
|
279
|
-
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
280
|
-
|
|
281
|
-
## Clone your solution from Power Apps
|
|
282
|
-
Run this command first (replace MySolution with your solution name):
|
|
283
|
-
pac solution clone --name MySolution --outputDirectory ./solutions
|
|
284
|
-
|
|
285
|
-
## Then ask the AI:
|
|
286
|
-
"I've cloned a Model-Driven App solution into the solutions/ folder.
|
|
287
|
-
Explain the folder structure and what each file does."
|
|
288
|
-
|
|
289
|
-
"Add a new form field called [FieldName] (type: text/date/lookup) to the [EntityName] main form."
|
|
290
|
-
|
|
291
|
-
"Create a business rule on the [EntityName] entity that [describe the rule]."
|
|
292
|
-
`);
|
|
293
|
-
|
|
294
|
-
log.ok("Model-Driven App structure created");
|
|
295
|
-
log.dim("Next: pac solution clone --name YourSolution --outputDirectory ./solutions");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function scaffoldPCF(dir, projectName, aiTool) {
|
|
299
|
-
log.info("Setting up PCF Code Component structure...");
|
|
300
|
-
|
|
301
|
-
write(path.join(dir, "instructions", "pcf-instructions.md"), `# PCF Code Component Instructions
|
|
302
|
-
|
|
303
|
-
## Type: Power Apps Component Framework (PCF)
|
|
304
|
-
## Project: ${projectName}
|
|
305
|
-
|
|
306
|
-
- Entry point is always index.ts implementing ComponentFramework.StandardControl
|
|
307
|
-
- Properties are declared in ControlManifest.Input.xml
|
|
308
|
-
- Use context.parameters to read input properties
|
|
309
|
-
- Use notifyOutputChanged() to push values back to the app
|
|
310
|
-
- Styling: use CSS modules or inline styles — no Tailwind
|
|
311
|
-
- Testing: use @testing-library/react + Jest
|
|
312
|
-
- Build: npm run build then pac pcf push
|
|
313
|
-
- Never modify .pcfproj or Solution.xml by hand
|
|
314
|
-
`);
|
|
315
|
-
|
|
316
|
-
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
317
|
-
|
|
318
|
-
## Initialise the PCF project (run this command first):
|
|
319
|
-
pac pcf init --namespace YourCompany --name ${projectName.replace(/\s+/g, "")} --template field
|
|
320
|
-
|
|
321
|
-
## Then ask the AI:
|
|
322
|
-
"I've initialised a PCF component called ${projectName}.
|
|
323
|
-
Explain the files that were created and what I need to edit to build my component."
|
|
324
|
-
|
|
325
|
-
"Build a PCF component that [describe what the component should do — e.g. shows a star rating control, renders a colour picker, displays a custom chart]."
|
|
326
|
-
|
|
327
|
-
"Add a property to this PCF component called [PropertyName] of type [SingleLine.Text / Whole.None / etc] that [describe what it controls]."
|
|
328
|
-
`);
|
|
329
|
-
|
|
330
|
-
log.ok("PCF structure created");
|
|
331
|
-
log.dim("Next: pac pcf init --namespace YourNamespace --name " + projectName.replace(/\s+/g, "") + " --template field");
|
|
332
|
-
log.dim("Then: npm install && npm start watch");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function scaffoldPowerPages(dir, projectName, aiTool) {
|
|
336
|
-
log.info("Setting up Power Pages structure...");
|
|
337
|
-
|
|
338
|
-
write(path.join(dir, "instructions", "powerpages-instructions.md"), `# Power Pages Instructions
|
|
339
|
-
|
|
340
|
-
## Type: Power Pages (External Portal)
|
|
341
|
-
## Project: ${projectName}
|
|
342
|
-
|
|
343
|
-
- Pages use Liquid templating language — similar to Jinja2
|
|
344
|
-
- Data access uses FetchXML or Liquid tags like {% fetchxml %}
|
|
345
|
-
- CSS and JS go in web-files/ — reference them in templates
|
|
346
|
-
- Authentication is managed by Power Pages — do not build custom auth
|
|
347
|
-
- Table permissions control data access — configure in Power Pages studio
|
|
348
|
-
- Use pac pages download / pac pages upload to sync changes
|
|
349
|
-
- Never hardcode environment URLs — use Liquid {{ request.url }} helpers
|
|
350
|
-
`);
|
|
351
|
-
|
|
352
|
-
write(path.join(dir, "site", "web-files", ".gitkeep"), "");
|
|
353
|
-
write(path.join(dir, "site", "web-pages", ".gitkeep"), "");
|
|
354
|
-
write(path.join(dir, "site", "web-templates", ".gitkeep"), "");
|
|
355
|
-
write(path.join(dir, "site", "content-snippets", ".gitkeep"), "");
|
|
356
|
-
write(path.join(dir, "site", "entity-forms", ".gitkeep"), "");
|
|
357
|
-
write(path.join(dir, "site", "web-roles", ".gitkeep"), "");
|
|
358
|
-
|
|
359
|
-
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
360
|
-
|
|
361
|
-
## Download your site from Power Apps (run first — replace the ID):
|
|
362
|
-
pac pages download --path ./site --webSiteId your-website-id
|
|
363
|
-
|
|
364
|
-
## Then ask the AI:
|
|
365
|
-
"I've downloaded a Power Pages site into the site/ folder.
|
|
366
|
-
Explain the folder structure and what each folder contains."
|
|
367
|
-
|
|
368
|
-
"Create a new page called [PageName] that [describe what the page does — e.g. shows a contact form, lists products from a Dataverse table]."
|
|
369
|
-
|
|
370
|
-
"Add a navigation link called [LinkName] to the site header that goes to /[page-url]."
|
|
371
|
-
`);
|
|
372
|
-
|
|
373
|
-
log.ok("Power Pages structure created");
|
|
374
|
-
log.dim("Next: pac pages download --path ./site --webSiteId <your-site-id>");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function scaffoldCodeApp(dir, projectName, aiTool) {
|
|
378
|
-
log.info("Setting up Code App structure...");
|
|
379
|
-
|
|
380
|
-
const safeName = projectName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
381
|
-
|
|
382
|
-
// ── package.json ──────────────────────────────────────────────────────────
|
|
383
|
-
// Uses Vite (not react-scripts/CRA which is deprecated and broken).
|
|
384
|
-
// @microsoft/power-apps is intentionally NOT listed here — it bundles
|
|
385
|
-
// a native addon (keytar) that fails to build on most machines.
|
|
386
|
-
// It is installed globally via: npm install -g @microsoft/power-apps
|
|
387
|
-
// and imported in app code once the user is ready to connect to Power Platform.
|
|
388
|
-
write(path.join(dir, "package.json"), JSON.stringify({
|
|
389
|
-
name: safeName,
|
|
390
|
-
version: "0.1.0",
|
|
391
|
-
private: true,
|
|
392
|
-
type: "module",
|
|
393
|
-
scripts: {
|
|
394
|
-
start: "vite",
|
|
395
|
-
build: "tsc && vite build",
|
|
396
|
-
preview: "vite preview",
|
|
397
|
-
},
|
|
398
|
-
dependencies: {
|
|
399
|
-
react: "^18.3.1",
|
|
400
|
-
"react-dom": "^18.3.1",
|
|
401
|
-
},
|
|
402
|
-
devDependencies: {
|
|
403
|
-
"@types/react": "^18.3.3",
|
|
404
|
-
"@types/react-dom": "^18.3.0",
|
|
405
|
-
"@vitejs/plugin-react": "^6.0.0",
|
|
406
|
-
typescript: "^5.5.3",
|
|
407
|
-
vite: "^8.0.0",
|
|
408
|
-
},
|
|
409
|
-
}, null, 2));
|
|
410
|
-
|
|
411
|
-
// ── vite.config.ts ────────────────────────────────────────────────────────
|
|
412
|
-
write(path.join(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
|
|
413
|
-
import react from '@vitejs/plugin-react'
|
|
414
|
-
|
|
415
|
-
// https://vitejs.dev/config/
|
|
416
|
-
export default defineConfig({
|
|
417
|
-
plugins: [react()],
|
|
418
|
-
server: {
|
|
419
|
-
port: 3000,
|
|
420
|
-
open: true, // automatically opens the browser on npm start
|
|
421
|
-
},
|
|
422
|
-
})
|
|
423
|
-
`);
|
|
424
|
-
|
|
425
|
-
// ── tsconfig.json ─────────────────────────────────────────────────────────
|
|
426
|
-
write(path.join(dir, "tsconfig.json"), JSON.stringify({
|
|
427
|
-
compilerOptions: {
|
|
428
|
-
target: "ES2020",
|
|
429
|
-
useDefineForClassFields: true,
|
|
430
|
-
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
|
431
|
-
module: "ESNext",
|
|
432
|
-
skipLibCheck: true,
|
|
433
|
-
moduleResolution: "bundler",
|
|
434
|
-
allowImportingTsExtensions: true,
|
|
435
|
-
isolatedModules: true,
|
|
436
|
-
moduleDetection: "force",
|
|
437
|
-
noEmit: true,
|
|
438
|
-
jsx: "react-jsx",
|
|
439
|
-
strict: true,
|
|
440
|
-
noUnusedLocals: true,
|
|
441
|
-
noUnusedParameters: true,
|
|
442
|
-
noFallthroughCasesInSwitch: true,
|
|
443
|
-
},
|
|
444
|
-
include: ["src"],
|
|
445
|
-
}, null, 2));
|
|
446
|
-
|
|
447
|
-
// ── index.html ────────────────────────────────────────────────────────────
|
|
448
|
-
// Vite requires index.html at the project root (not inside /public)
|
|
449
|
-
write(path.join(dir, "index.html"), `<!DOCTYPE html>
|
|
450
|
-
<html lang="en">
|
|
451
|
-
<head>
|
|
452
|
-
<meta charset="UTF-8" />
|
|
453
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
454
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
455
|
-
<title>${projectName}</title>
|
|
456
|
-
</head>
|
|
457
|
-
<body>
|
|
458
|
-
<div id="root"></div>
|
|
459
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
460
|
-
</body>
|
|
461
|
-
</html>
|
|
462
|
-
`);
|
|
463
|
-
|
|
464
|
-
// ── public/favicon.svg ────────────────────────────────────────────────────
|
|
465
|
-
write(path.join(dir, "public", "favicon.svg"), `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
466
|
-
<rect width="32" height="32" rx="6" fill="#742774"/>
|
|
467
|
-
<text x="16" y="22" text-anchor="middle" font-size="18" fill="white" font-family="sans-serif">⚡</text>
|
|
468
|
-
</svg>
|
|
469
|
-
`);
|
|
470
|
-
|
|
471
|
-
// ── src/vite-env.d.ts ─────────────────────────────────────────────────────
|
|
472
|
-
// Required: tells TypeScript what *.module.css imports look like
|
|
473
|
-
write(path.join(dir, "src", "vite-env.d.ts"), `/// <reference types="vite/client" />
|
|
474
|
-
|
|
475
|
-
// CSS Modules
|
|
476
|
-
declare module '*.module.css' {
|
|
477
|
-
const classes: Record<string, string>
|
|
478
|
-
export default classes
|
|
479
|
-
}
|
|
480
|
-
`);
|
|
481
|
-
|
|
482
|
-
// ── src/main.tsx ──────────────────────────────────────────────────────────
|
|
483
|
-
write(path.join(dir, "src", "main.tsx"), `import { StrictMode } from 'react'
|
|
484
|
-
import { createRoot } from 'react-dom/client'
|
|
485
|
-
import App from './App'
|
|
486
|
-
import './index.css'
|
|
487
|
-
|
|
488
|
-
createRoot(document.getElementById('root')!).render(
|
|
489
|
-
<StrictMode>
|
|
490
|
-
<App />
|
|
491
|
-
</StrictMode>
|
|
492
|
-
)
|
|
493
|
-
`);
|
|
494
|
-
|
|
495
|
-
// ── src/App.tsx ───────────────────────────────────────────────────────────
|
|
496
|
-
write(path.join(dir, "src", "App.tsx"), `import styles from './App.module.css'
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* ${projectName}
|
|
500
|
-
*
|
|
501
|
-
* This is your app's root component. Start here.
|
|
502
|
-
*
|
|
503
|
-
* To connect to Power Platform data and connectors:
|
|
504
|
-
* 1. Install the CLI globally: npm install -g @microsoft/power-apps
|
|
505
|
-
* 2. Run: npx power-apps init (creates power.config.json)
|
|
506
|
-
* 3. Then import in your components:
|
|
507
|
-
* import { useData } from '@microsoft/power-apps'
|
|
508
|
-
*
|
|
509
|
-
* Ask your AI assistant: "Build the main layout for ${projectName}"
|
|
510
|
-
*/
|
|
511
|
-
export default function App() {
|
|
512
|
-
return (
|
|
513
|
-
<div className={styles.app}>
|
|
514
|
-
<header className={styles.header}>
|
|
515
|
-
<h1>${projectName}</h1>
|
|
516
|
-
<p>Your Power Apps Code App is running. Open your AI assistant and describe what to build.</p>
|
|
517
|
-
</header>
|
|
518
|
-
<main className={styles.main}>
|
|
519
|
-
<div className={styles.card}>
|
|
520
|
-
<h2>🚀 Ready to build</h2>
|
|
521
|
-
<p>Open Copilot Chat or Claude Code and paste a prompt from <code>prompts/starter.md</code></p>
|
|
522
|
-
</div>
|
|
523
|
-
</main>
|
|
524
|
-
</div>
|
|
525
|
-
)
|
|
526
|
-
}
|
|
527
|
-
`);
|
|
528
|
-
|
|
529
|
-
// ── src/App.module.css ────────────────────────────────────────────────────
|
|
530
|
-
write(path.join(dir, "src", "App.module.css"), `.app {
|
|
531
|
-
min-height: 100vh;
|
|
532
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
533
|
-
background: #f4f4f8;
|
|
534
|
-
color: #1a1a2e;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
.header {
|
|
538
|
-
background: #742774;
|
|
539
|
-
color: white;
|
|
540
|
-
padding: 2rem;
|
|
541
|
-
text-align: center;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
.header h1 {
|
|
545
|
-
margin: 0 0 0.5rem;
|
|
546
|
-
font-size: 2rem;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
.header p {
|
|
550
|
-
margin: 0;
|
|
551
|
-
opacity: 0.85;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
.main {
|
|
555
|
-
max-width: 800px;
|
|
556
|
-
margin: 2rem auto;
|
|
557
|
-
padding: 0 1rem;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
.card {
|
|
561
|
-
background: white;
|
|
562
|
-
border-radius: 8px;
|
|
563
|
-
padding: 1.5rem 2rem;
|
|
564
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
.card h2 { margin-top: 0; }
|
|
568
|
-
`);
|
|
569
|
-
|
|
570
|
-
// ── src/index.css ─────────────────────────────────────────────────────────
|
|
571
|
-
write(path.join(dir, "src", "index.css"), `*, *::before, *::after { box-sizing: border-box; }
|
|
572
|
-
body { margin: 0; }
|
|
573
|
-
`);
|
|
574
|
-
|
|
575
|
-
// ── src/components/ placeholder ───────────────────────────────────────────
|
|
576
|
-
write(path.join(dir, "src", "components", ".gitkeep"), "");
|
|
577
|
-
|
|
578
|
-
// ── instructions ─────────────────────────────────────────────────────────
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
##
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
-
|
|
588
|
-
- React
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
-
|
|
602
|
-
-
|
|
603
|
-
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
- A
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
log.
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
},
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
log.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
case "Model-Driven App": scaffoldMDA(dir, projectName, aiChoice.label); break;
|
|
870
|
-
case "PCF Component": scaffoldPCF(dir, projectName, aiChoice.label); break;
|
|
871
|
-
case "Power Pages": scaffoldPowerPages(dir, projectName, aiChoice.label); break;
|
|
872
|
-
case "Code App": scaffoldCodeApp(dir, projectName, aiChoice.label); break;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// ── 8. AI context file ───────────────────────────────────────────────────
|
|
876
|
-
log.step("Creating AI context file...");
|
|
877
|
-
writeAIContextFile(dir, projectName, appTypeChoice.label, aiChoice.label);
|
|
878
|
-
|
|
879
|
-
// ── 9. Git init ──────────────────────────────────────────────────────────
|
|
880
|
-
log.step("Initialising Git...");
|
|
881
|
-
if (has("git")) {
|
|
882
|
-
run("git init", dir, true);
|
|
883
|
-
run("git add .", dir, true);
|
|
884
|
-
run('git commit -m "Initial scaffold by create-powerapp"', dir, true);
|
|
885
|
-
log.ok("Git initialised with first commit");
|
|
886
|
-
} else {
|
|
887
|
-
log.warn("Git not found — skipping. Install Git and run git init inside your project.");
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// ── 10. PAC CLI auth ─────────────────────────────────────────────────────
|
|
891
|
-
if (envUrl) {
|
|
892
|
-
log.step("Connecting to Power Platform environment...");
|
|
893
|
-
if (has("pac")) {
|
|
894
|
-
log.info(`Running: pac auth create --url ${envUrl}`);
|
|
895
|
-
log.dim("A browser window will open. Sign in with your Microsoft work account.");
|
|
896
|
-
run(`pac auth create --url ${envUrl}`, dir);
|
|
897
|
-
} else {
|
|
898
|
-
log.warn("PAC CLI not found. Install it with: npm install -g pac");
|
|
899
|
-
log.warn("Then run: pac auth create --url " + envUrl);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// ── 11. Open in VS Code ──────────────────────────────────────────────────
|
|
904
|
-
log.step("Opening in VS Code...");
|
|
905
|
-
if (has("code")) {
|
|
906
|
-
run(`code "${dir}"`, process.cwd(), true);
|
|
907
|
-
log.ok("VS Code opened with your project");
|
|
908
|
-
} else {
|
|
909
|
-
log.warn("VS Code code command not found.");
|
|
910
|
-
log.dim("Open VS Code manually → File → Open Folder → select: " + dir);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// ── Done ─────────────────────────────────────────────────────────────────
|
|
914
|
-
<<<<<<< HEAD
|
|
915
|
-
const isCodeApp = appTypeChoice.label === "Code App";
|
|
916
|
-
=======
|
|
917
|
-
>>>>>>> 3a159cf4be27d864191588e82e9143d5857edcac
|
|
918
|
-
console.log(`
|
|
919
|
-
${c.bold}${c.green}╔═══════════════════════════════════════════════════╗
|
|
920
|
-
║ ✅ Your project is ready! ║
|
|
921
|
-
╚═══════════════════════════════════════════════════╝${c.reset}
|
|
922
|
-
|
|
923
|
-
${c.bold}Project folder:${c.reset} ${dir}
|
|
924
|
-
|
|
925
|
-
${c.bold}What to do next:${c.reset}
|
|
926
|
-
|
|
927
|
-
1. VS Code is now open (or open it manually and open the folder above)
|
|
928
|
-
<<<<<<< HEAD
|
|
929
|
-
${isCodeApp ? `
|
|
930
|
-
2. ${c.bold}${c.green}Start the app:${c.reset} In the VS Code terminal, run:
|
|
931
|
-
${c.cyan}npm start${c.reset}
|
|
932
|
-
Your browser will open automatically at http://localhost:3000
|
|
933
|
-
` : `
|
|
934
|
-
2. Read the ${c.cyan}prompts/starter.md${c.reset} file for ready-made prompts to get started
|
|
935
|
-
`}
|
|
936
|
-
=======
|
|
937
|
-
|
|
938
|
-
2. Read the ${c.cyan}prompts/starter.md${c.reset} file for ready-made prompts to get started
|
|
939
|
-
|
|
940
|
-
>>>>>>> 3a159cf4be27d864191588e82e9143d5857edcac
|
|
941
|
-
3. Open your AI assistant:
|
|
942
|
-
${aiChoice.label === "Claude Code"
|
|
943
|
-
? ` • In the VS Code terminal, run: ${c.cyan}claude${c.reset}
|
|
944
|
-
• Or click the Claude Code icon in the left sidebar`
|
|
945
|
-
: ` • Click the Copilot Chat icon (💬) in the VS Code left sidebar`}
|
|
946
|
-
|
|
947
|
-
4. Copy a prompt from ${c.cyan}prompts/starter.md${c.reset} and paste it in — or describe your app in your own words
|
|
948
|
-
|
|
949
|
-
5. When you're happy with what the AI built, save your work:
|
|
950
|
-
${c.dim}git add . → git commit -m "describe what you built" → git push${c.reset}
|
|
951
|
-
|
|
952
|
-
${c.bold}${c.yellow}Tip:${c.reset} When something goes wrong, tell the AI:
|
|
953
|
-
"I got this error: [paste error]. What does it mean and how do I fix it?"
|
|
954
|
-
|
|
955
|
-
Happy building! 🚀
|
|
956
|
-
`);
|
|
957
|
-
rl.close();
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
main().catch((err) => {
|
|
961
|
-
log.error("Unexpected error: " + err.message);
|
|
962
|
-
rl.close();
|
|
963
|
-
process.exit(1);
|
|
964
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-powerapp
|
|
5
|
+
* One command to scaffold any Power Apps project — Canvas App, Model-Driven App,
|
|
6
|
+
* PCF Component, Power Pages, or Code App — with Git, AI context files, and
|
|
7
|
+
* everything ready so you can immediately start prompting your AI assistant.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync, spawnSync } from "child_process";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import readline from "readline";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// ─── Colour helpers ──────────────────────────────────────────────────────────
|
|
19
|
+
const c = {
|
|
20
|
+
reset: "\x1b[0m",
|
|
21
|
+
bold: "\x1b[1m",
|
|
22
|
+
green: "\x1b[32m",
|
|
23
|
+
cyan: "\x1b[36m",
|
|
24
|
+
yellow: "\x1b[33m",
|
|
25
|
+
red: "\x1b[31m",
|
|
26
|
+
dim: "\x1b[2m",
|
|
27
|
+
magenta: "\x1b[35m",
|
|
28
|
+
};
|
|
29
|
+
const log = {
|
|
30
|
+
info: (m) => console.log(`${c.cyan}ℹ${c.reset} ${m}`),
|
|
31
|
+
ok: (m) => console.log(`${c.green}✔${c.reset} ${m}`),
|
|
32
|
+
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
33
|
+
error: (m) => console.log(`${c.red}✖${c.reset} ${m}`),
|
|
34
|
+
step: (m) => console.log(`\n${c.bold}${c.magenta}▶ ${m}${c.reset}`),
|
|
35
|
+
dim: (m) => console.log(`${c.dim} ${m}${c.reset}`),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ─── readline helper ─────────────────────────────────────────────────────────
|
|
39
|
+
// Single persistent readline interface — works interactively AND when piped (CI)
|
|
40
|
+
const rl = readline.createInterface({
|
|
41
|
+
input: process.stdin,
|
|
42
|
+
output: process.stdout,
|
|
43
|
+
terminal: false, // don't assume TTY — makes piped input work in CI
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Queue of lines already read from stdin (for piped / non-interactive mode)
|
|
47
|
+
const lineQueue = [];
|
|
48
|
+
const lineWaiters = [];
|
|
49
|
+
rl.on("line", (line) => {
|
|
50
|
+
if (lineWaiters.length > 0) {
|
|
51
|
+
lineWaiters.shift()(line.trim());
|
|
52
|
+
} else {
|
|
53
|
+
lineQueue.push(line.trim());
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function prompt(question) {
|
|
58
|
+
process.stdout.write(question);
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
if (lineQueue.length > 0) {
|
|
61
|
+
const line = lineQueue.shift();
|
|
62
|
+
process.stdout.write(line + "\n"); // echo the pre-buffered answer
|
|
63
|
+
resolve(line);
|
|
64
|
+
} else {
|
|
65
|
+
lineWaiters.push(resolve);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function choose(question, options) {
|
|
71
|
+
console.log(`\n${c.bold}${question}${c.reset}`);
|
|
72
|
+
options.forEach((o, i) => console.log(` ${c.cyan}${i + 1}${c.reset}. ${o.label} ${c.dim}${o.desc || ""}${c.reset}`));
|
|
73
|
+
while (true) {
|
|
74
|
+
const answer = await prompt(`\nEnter number (1–${options.length}): `);
|
|
75
|
+
const idx = parseInt(answer, 10) - 1;
|
|
76
|
+
if (idx >= 0 && idx < options.length) return options[idx];
|
|
77
|
+
log.warn("Please enter a valid number.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── run shell command ────────────────────────────────────────────────────────
|
|
82
|
+
function run(cmd, cwd = process.cwd(), silent = false) {
|
|
83
|
+
const result = spawnSync(cmd, { shell: true, cwd, stdio: silent ? "pipe" : "inherit" });
|
|
84
|
+
if (result.status !== 0 && !silent) {
|
|
85
|
+
log.error(`Command failed: ${cmd}`);
|
|
86
|
+
}
|
|
87
|
+
return result.status === 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── check a CLI tool exists ──────────────────────────────────────────────────
|
|
91
|
+
function has(tool) {
|
|
92
|
+
return spawnSync(`${tool} --version`, { shell: true, stdio: "pipe" }).status === 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── write file helper ────────────────────────────────────────────────────────
|
|
96
|
+
function write(filePath, content) {
|
|
97
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
98
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── shared file content ──────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function gitignoreContent() {
|
|
104
|
+
return `# Dependencies
|
|
105
|
+
node_modules/
|
|
106
|
+
.pnp
|
|
107
|
+
.pnp.js
|
|
108
|
+
|
|
109
|
+
# Environment variables — NEVER commit these
|
|
110
|
+
.env
|
|
111
|
+
.env.local
|
|
112
|
+
.env.*.local
|
|
113
|
+
|
|
114
|
+
# Build outputs
|
|
115
|
+
dist/
|
|
116
|
+
build/
|
|
117
|
+
out/
|
|
118
|
+
.next/
|
|
119
|
+
bin/
|
|
120
|
+
|
|
121
|
+
# Editor files
|
|
122
|
+
.vscode/
|
|
123
|
+
.idea/
|
|
124
|
+
*.swp
|
|
125
|
+
|
|
126
|
+
# OS files
|
|
127
|
+
.DS_Store
|
|
128
|
+
Thumbs.db
|
|
129
|
+
|
|
130
|
+
# Logs
|
|
131
|
+
*.log
|
|
132
|
+
npm-debug.log*
|
|
133
|
+
|
|
134
|
+
# Power Apps / PAC CLI outputs
|
|
135
|
+
*.msapp
|
|
136
|
+
*.zip
|
|
137
|
+
/obj/
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function envContent(projectName, appType) {
|
|
142
|
+
return `# .env — DO NOT commit this file to Git
|
|
143
|
+
# Fill in the values for your project
|
|
144
|
+
|
|
145
|
+
# Your Power Platform environment URL
|
|
146
|
+
# Find it at: make.powerapps.com → Settings gear → Session details → Instance url
|
|
147
|
+
POWER_PLATFORM_URL=https://yourorg.crm.dynamics.com
|
|
148
|
+
|
|
149
|
+
# Project info
|
|
150
|
+
PROJECT_NAME=${projectName}
|
|
151
|
+
APP_TYPE=${appType}
|
|
152
|
+
|
|
153
|
+
# Add any other secrets here (API keys, database URLs, etc.)
|
|
154
|
+
# API_KEY=your-key-here
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readmeContent(projectName, appType, aiTool) {
|
|
159
|
+
return `# ${projectName}
|
|
160
|
+
|
|
161
|
+
**Type:** ${appType}
|
|
162
|
+
**AI Assistant:** ${aiTool}
|
|
163
|
+
|
|
164
|
+
## Getting started
|
|
165
|
+
|
|
166
|
+
Everything is already set up. Just open your AI assistant and describe what you want to build.
|
|
167
|
+
|
|
168
|
+
### Start your AI session
|
|
169
|
+
|
|
170
|
+
${aiTool === "Claude Code"
|
|
171
|
+
? "```bash\nclaude\n```\nThen type what you want to build in plain English."
|
|
172
|
+
: "Click the **Copilot Chat** icon (💬) in the VS Code sidebar and type what you want to build."}
|
|
173
|
+
|
|
174
|
+
### Example first prompts
|
|
175
|
+
|
|
176
|
+
> "Create the main screen for this app with a navigation menu and a welcome message."
|
|
177
|
+
|
|
178
|
+
> "Add a form that lets users submit [whatever your app needs] and saves it to Dataverse."
|
|
179
|
+
|
|
180
|
+
> "Explain what files are in this project and what each one does."
|
|
181
|
+
|
|
182
|
+
## Project structure
|
|
183
|
+
|
|
184
|
+
See the \`instructions/\` folder for the AI context files that tell your AI assistant
|
|
185
|
+
how this type of Power App works.
|
|
186
|
+
|
|
187
|
+
## Useful commands
|
|
188
|
+
|
|
189
|
+
\`\`\`bash
|
|
190
|
+
# Push changes to Power Platform
|
|
191
|
+
${appType === "Canvas App" ? "pac canvas pack --msapp ./CanvasApp.msapp --sources ./canvas-src\npac solution push" : ""}
|
|
192
|
+
${appType === "Model-Driven App" ? "pac solution push" : ""}
|
|
193
|
+
${appType === "PCF Component" ? "npm run build\npac pcf push --publisher-prefix yourprefix" : ""}
|
|
194
|
+
${appType === "Power Pages" ? "pac pages upload --path ." : ""}
|
|
195
|
+
${appType === "Code App" ? "npm run build\npac code push" : ""}
|
|
196
|
+
|
|
197
|
+
# Save your work
|
|
198
|
+
git add .
|
|
199
|
+
git commit -m "describe what you changed"
|
|
200
|
+
git push
|
|
201
|
+
\`\`\`
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── App-type scaffolders ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function scaffoldCanvas(dir, projectName, aiTool) {
|
|
208
|
+
log.info("Setting up Canvas App structure...");
|
|
209
|
+
|
|
210
|
+
// AI context files
|
|
211
|
+
write(path.join(dir, "instructions", "canvas-instructions.md"), `# Canvas App Instructions
|
|
212
|
+
|
|
213
|
+
## Type: Canvas App (Power Apps)
|
|
214
|
+
## Project: ${projectName}
|
|
215
|
+
|
|
216
|
+
- Screens and controls are defined in .fx.yaml files
|
|
217
|
+
- Formulas use Power Fx — similar to Excel syntax
|
|
218
|
+
- Use With(), Collect(), Patch() for data operations — never SQL
|
|
219
|
+
- Navigation: Navigate(ScreenName, ScreenTransition.Fade)
|
|
220
|
+
- Avoid nested galleries — use collections instead
|
|
221
|
+
- PAC commands: pac canvas unpack / pac canvas pack
|
|
222
|
+
- Never edit .msapp binary directly — always unpack first
|
|
223
|
+
- Assets (images, icons) go in Assets/
|
|
224
|
+
- Data source connections go in DataSources/
|
|
225
|
+
`);
|
|
226
|
+
|
|
227
|
+
write(path.join(dir, "canvas-src", "Src", ".gitkeep"), "");
|
|
228
|
+
write(path.join(dir, "canvas-src", "Assets", ".gitkeep"), "");
|
|
229
|
+
write(path.join(dir, "canvas-src", "DataSources", ".gitkeep"), "");
|
|
230
|
+
write(path.join(dir, "canvas-src", "pkgs", ".gitkeep"), "");
|
|
231
|
+
|
|
232
|
+
write(path.join(dir, "canvas-src", "Src", "App.fx.yaml"), `# App-level settings and global variables
|
|
233
|
+
# The AI will help you fill this in as you build
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
237
|
+
|
|
238
|
+
Copy and paste these into your AI assistant to get started quickly.
|
|
239
|
+
|
|
240
|
+
## Create your first screen
|
|
241
|
+
"I'm building a Canvas App called ${projectName}. Create the Home screen with:
|
|
242
|
+
- A header showing the app title and the current user's name
|
|
243
|
+
- A navigation menu with buttons for the main sections of the app
|
|
244
|
+
- A clean, professional layout using neutral colours"
|
|
245
|
+
|
|
246
|
+
## Add a data form
|
|
247
|
+
"Add a new screen with a form that lets users enter [describe your data].
|
|
248
|
+
Save it to a Dataverse table called [TableName]. Include validation so required fields can't be empty."
|
|
249
|
+
|
|
250
|
+
## Connect to data
|
|
251
|
+
"Connect this app to a SharePoint list called [ListName] at [SharePoint URL].
|
|
252
|
+
Show the items in a gallery on the Home screen, sorted by most recent first."
|
|
253
|
+
`);
|
|
254
|
+
|
|
255
|
+
log.ok("Canvas App structure created");
|
|
256
|
+
log.dim("Next: run pac canvas unpack --msapp YourApp.msapp --sources ./canvas-src to import an existing app");
|
|
257
|
+
log.dim("Or ask your AI: 'Create the first screen for a Canvas App called " + projectName + "'");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function scaffoldMDA(dir, projectName, aiTool) {
|
|
261
|
+
log.info("Setting up Model-Driven App structure...");
|
|
262
|
+
|
|
263
|
+
write(path.join(dir, "instructions", "mda-instructions.md"), `# Model-Driven App Instructions
|
|
264
|
+
|
|
265
|
+
## Type: Model-Driven App (Power Apps / Dataverse)
|
|
266
|
+
## Project: ${projectName}
|
|
267
|
+
|
|
268
|
+
- All entities map to Dataverse tables
|
|
269
|
+
- Forms are defined in XML — do not hand-edit GUIDs
|
|
270
|
+
- Business rules go in Dataverse, not client-side JS
|
|
271
|
+
- Use PAC CLI to push/pull: pac solution push / pac solution clone
|
|
272
|
+
- Plugin code goes in the Plugins/ folder, registered via Plugin Registration Tool
|
|
273
|
+
- JavaScript web resources live in WebResources/ and must be registered on forms
|
|
274
|
+
- SiteMaps define the app navigation structure
|
|
275
|
+
`);
|
|
276
|
+
|
|
277
|
+
write(path.join(dir, "solutions", ".gitkeep"), "");
|
|
278
|
+
|
|
279
|
+
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
280
|
+
|
|
281
|
+
## Clone your solution from Power Apps
|
|
282
|
+
Run this command first (replace MySolution with your solution name):
|
|
283
|
+
pac solution clone --name MySolution --outputDirectory ./solutions
|
|
284
|
+
|
|
285
|
+
## Then ask the AI:
|
|
286
|
+
"I've cloned a Model-Driven App solution into the solutions/ folder.
|
|
287
|
+
Explain the folder structure and what each file does."
|
|
288
|
+
|
|
289
|
+
"Add a new form field called [FieldName] (type: text/date/lookup) to the [EntityName] main form."
|
|
290
|
+
|
|
291
|
+
"Create a business rule on the [EntityName] entity that [describe the rule]."
|
|
292
|
+
`);
|
|
293
|
+
|
|
294
|
+
log.ok("Model-Driven App structure created");
|
|
295
|
+
log.dim("Next: pac solution clone --name YourSolution --outputDirectory ./solutions");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function scaffoldPCF(dir, projectName, aiTool) {
|
|
299
|
+
log.info("Setting up PCF Code Component structure...");
|
|
300
|
+
|
|
301
|
+
write(path.join(dir, "instructions", "pcf-instructions.md"), `# PCF Code Component Instructions
|
|
302
|
+
|
|
303
|
+
## Type: Power Apps Component Framework (PCF)
|
|
304
|
+
## Project: ${projectName}
|
|
305
|
+
|
|
306
|
+
- Entry point is always index.ts implementing ComponentFramework.StandardControl
|
|
307
|
+
- Properties are declared in ControlManifest.Input.xml
|
|
308
|
+
- Use context.parameters to read input properties
|
|
309
|
+
- Use notifyOutputChanged() to push values back to the app
|
|
310
|
+
- Styling: use CSS modules or inline styles — no Tailwind
|
|
311
|
+
- Testing: use @testing-library/react + Jest
|
|
312
|
+
- Build: npm run build then pac pcf push
|
|
313
|
+
- Never modify .pcfproj or Solution.xml by hand
|
|
314
|
+
`);
|
|
315
|
+
|
|
316
|
+
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
317
|
+
|
|
318
|
+
## Initialise the PCF project (run this command first):
|
|
319
|
+
pac pcf init --namespace YourCompany --name ${projectName.replace(/\s+/g, "")} --template field
|
|
320
|
+
|
|
321
|
+
## Then ask the AI:
|
|
322
|
+
"I've initialised a PCF component called ${projectName}.
|
|
323
|
+
Explain the files that were created and what I need to edit to build my component."
|
|
324
|
+
|
|
325
|
+
"Build a PCF component that [describe what the component should do — e.g. shows a star rating control, renders a colour picker, displays a custom chart]."
|
|
326
|
+
|
|
327
|
+
"Add a property to this PCF component called [PropertyName] of type [SingleLine.Text / Whole.None / etc] that [describe what it controls]."
|
|
328
|
+
`);
|
|
329
|
+
|
|
330
|
+
log.ok("PCF structure created");
|
|
331
|
+
log.dim("Next: pac pcf init --namespace YourNamespace --name " + projectName.replace(/\s+/g, "") + " --template field");
|
|
332
|
+
log.dim("Then: npm install && npm start watch");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function scaffoldPowerPages(dir, projectName, aiTool) {
|
|
336
|
+
log.info("Setting up Power Pages structure...");
|
|
337
|
+
|
|
338
|
+
write(path.join(dir, "instructions", "powerpages-instructions.md"), `# Power Pages Instructions
|
|
339
|
+
|
|
340
|
+
## Type: Power Pages (External Portal)
|
|
341
|
+
## Project: ${projectName}
|
|
342
|
+
|
|
343
|
+
- Pages use Liquid templating language — similar to Jinja2
|
|
344
|
+
- Data access uses FetchXML or Liquid tags like {% fetchxml %}
|
|
345
|
+
- CSS and JS go in web-files/ — reference them in templates
|
|
346
|
+
- Authentication is managed by Power Pages — do not build custom auth
|
|
347
|
+
- Table permissions control data access — configure in Power Pages studio
|
|
348
|
+
- Use pac pages download / pac pages upload to sync changes
|
|
349
|
+
- Never hardcode environment URLs — use Liquid {{ request.url }} helpers
|
|
350
|
+
`);
|
|
351
|
+
|
|
352
|
+
write(path.join(dir, "site", "web-files", ".gitkeep"), "");
|
|
353
|
+
write(path.join(dir, "site", "web-pages", ".gitkeep"), "");
|
|
354
|
+
write(path.join(dir, "site", "web-templates", ".gitkeep"), "");
|
|
355
|
+
write(path.join(dir, "site", "content-snippets", ".gitkeep"), "");
|
|
356
|
+
write(path.join(dir, "site", "entity-forms", ".gitkeep"), "");
|
|
357
|
+
write(path.join(dir, "site", "web-roles", ".gitkeep"), "");
|
|
358
|
+
|
|
359
|
+
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
360
|
+
|
|
361
|
+
## Download your site from Power Apps (run first — replace the ID):
|
|
362
|
+
pac pages download --path ./site --webSiteId your-website-id
|
|
363
|
+
|
|
364
|
+
## Then ask the AI:
|
|
365
|
+
"I've downloaded a Power Pages site into the site/ folder.
|
|
366
|
+
Explain the folder structure and what each folder contains."
|
|
367
|
+
|
|
368
|
+
"Create a new page called [PageName] that [describe what the page does — e.g. shows a contact form, lists products from a Dataverse table]."
|
|
369
|
+
|
|
370
|
+
"Add a navigation link called [LinkName] to the site header that goes to /[page-url]."
|
|
371
|
+
`);
|
|
372
|
+
|
|
373
|
+
log.ok("Power Pages structure created");
|
|
374
|
+
log.dim("Next: pac pages download --path ./site --webSiteId <your-site-id>");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function scaffoldCodeApp(dir, projectName, aiTool) {
|
|
378
|
+
log.info("Setting up Code App structure...");
|
|
379
|
+
|
|
380
|
+
const safeName = projectName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
381
|
+
|
|
382
|
+
// ── package.json ──────────────────────────────────────────────────────────
|
|
383
|
+
// Uses Vite (not react-scripts/CRA which is deprecated and broken).
|
|
384
|
+
// @microsoft/power-apps is intentionally NOT listed here — it bundles
|
|
385
|
+
// a native addon (keytar) that fails to build on most machines.
|
|
386
|
+
// It is installed globally via: npm install -g @microsoft/power-apps
|
|
387
|
+
// and imported in app code once the user is ready to connect to Power Platform.
|
|
388
|
+
write(path.join(dir, "package.json"), JSON.stringify({
|
|
389
|
+
name: safeName,
|
|
390
|
+
version: "0.1.0",
|
|
391
|
+
private: true,
|
|
392
|
+
type: "module",
|
|
393
|
+
scripts: {
|
|
394
|
+
start: "vite",
|
|
395
|
+
build: "tsc && vite build",
|
|
396
|
+
preview: "vite preview",
|
|
397
|
+
},
|
|
398
|
+
dependencies: {
|
|
399
|
+
react: "^18.3.1",
|
|
400
|
+
"react-dom": "^18.3.1",
|
|
401
|
+
},
|
|
402
|
+
devDependencies: {
|
|
403
|
+
"@types/react": "^18.3.3",
|
|
404
|
+
"@types/react-dom": "^18.3.0",
|
|
405
|
+
"@vitejs/plugin-react": "^6.0.0",
|
|
406
|
+
typescript: "^5.5.3",
|
|
407
|
+
vite: "^8.0.0",
|
|
408
|
+
},
|
|
409
|
+
}, null, 2));
|
|
410
|
+
|
|
411
|
+
// ── vite.config.ts ────────────────────────────────────────────────────────
|
|
412
|
+
write(path.join(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
|
|
413
|
+
import react from '@vitejs/plugin-react'
|
|
414
|
+
|
|
415
|
+
// https://vitejs.dev/config/
|
|
416
|
+
export default defineConfig({
|
|
417
|
+
plugins: [react()],
|
|
418
|
+
server: {
|
|
419
|
+
port: 3000,
|
|
420
|
+
open: true, // automatically opens the browser on npm start
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
`);
|
|
424
|
+
|
|
425
|
+
// ── tsconfig.json ─────────────────────────────────────────────────────────
|
|
426
|
+
write(path.join(dir, "tsconfig.json"), JSON.stringify({
|
|
427
|
+
compilerOptions: {
|
|
428
|
+
target: "ES2020",
|
|
429
|
+
useDefineForClassFields: true,
|
|
430
|
+
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
|
431
|
+
module: "ESNext",
|
|
432
|
+
skipLibCheck: true,
|
|
433
|
+
moduleResolution: "bundler",
|
|
434
|
+
allowImportingTsExtensions: true,
|
|
435
|
+
isolatedModules: true,
|
|
436
|
+
moduleDetection: "force",
|
|
437
|
+
noEmit: true,
|
|
438
|
+
jsx: "react-jsx",
|
|
439
|
+
strict: true,
|
|
440
|
+
noUnusedLocals: true,
|
|
441
|
+
noUnusedParameters: true,
|
|
442
|
+
noFallthroughCasesInSwitch: true,
|
|
443
|
+
},
|
|
444
|
+
include: ["src"],
|
|
445
|
+
}, null, 2));
|
|
446
|
+
|
|
447
|
+
// ── index.html ────────────────────────────────────────────────────────────
|
|
448
|
+
// Vite requires index.html at the project root (not inside /public)
|
|
449
|
+
write(path.join(dir, "index.html"), `<!DOCTYPE html>
|
|
450
|
+
<html lang="en">
|
|
451
|
+
<head>
|
|
452
|
+
<meta charset="UTF-8" />
|
|
453
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
454
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
455
|
+
<title>${projectName}</title>
|
|
456
|
+
</head>
|
|
457
|
+
<body>
|
|
458
|
+
<div id="root"></div>
|
|
459
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
460
|
+
</body>
|
|
461
|
+
</html>
|
|
462
|
+
`);
|
|
463
|
+
|
|
464
|
+
// ── public/favicon.svg ────────────────────────────────────────────────────
|
|
465
|
+
write(path.join(dir, "public", "favicon.svg"), `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
466
|
+
<rect width="32" height="32" rx="6" fill="#742774"/>
|
|
467
|
+
<text x="16" y="22" text-anchor="middle" font-size="18" fill="white" font-family="sans-serif">⚡</text>
|
|
468
|
+
</svg>
|
|
469
|
+
`);
|
|
470
|
+
|
|
471
|
+
// ── src/vite-env.d.ts ─────────────────────────────────────────────────────
|
|
472
|
+
// Required: tells TypeScript what *.module.css imports look like
|
|
473
|
+
write(path.join(dir, "src", "vite-env.d.ts"), `/// <reference types="vite/client" />
|
|
474
|
+
|
|
475
|
+
// CSS Modules
|
|
476
|
+
declare module '*.module.css' {
|
|
477
|
+
const classes: Record<string, string>
|
|
478
|
+
export default classes
|
|
479
|
+
}
|
|
480
|
+
`);
|
|
481
|
+
|
|
482
|
+
// ── src/main.tsx ──────────────────────────────────────────────────────────
|
|
483
|
+
write(path.join(dir, "src", "main.tsx"), `import { StrictMode } from 'react'
|
|
484
|
+
import { createRoot } from 'react-dom/client'
|
|
485
|
+
import App from './App'
|
|
486
|
+
import './index.css'
|
|
487
|
+
|
|
488
|
+
createRoot(document.getElementById('root')!).render(
|
|
489
|
+
<StrictMode>
|
|
490
|
+
<App />
|
|
491
|
+
</StrictMode>
|
|
492
|
+
)
|
|
493
|
+
`);
|
|
494
|
+
|
|
495
|
+
// ── src/App.tsx ───────────────────────────────────────────────────────────
|
|
496
|
+
write(path.join(dir, "src", "App.tsx"), `import styles from './App.module.css'
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* ${projectName}
|
|
500
|
+
*
|
|
501
|
+
* This is your app's root component. Start here.
|
|
502
|
+
*
|
|
503
|
+
* To connect to Power Platform data and connectors:
|
|
504
|
+
* 1. Install the CLI globally: npm install -g @microsoft/power-apps
|
|
505
|
+
* 2. Run: npx power-apps init (creates power.config.json)
|
|
506
|
+
* 3. Then import in your components:
|
|
507
|
+
* import { useData } from '@microsoft/power-apps'
|
|
508
|
+
*
|
|
509
|
+
* Ask your AI assistant: "Build the main layout for ${projectName}"
|
|
510
|
+
*/
|
|
511
|
+
export default function App() {
|
|
512
|
+
return (
|
|
513
|
+
<div className={styles.app}>
|
|
514
|
+
<header className={styles.header}>
|
|
515
|
+
<h1>${projectName}</h1>
|
|
516
|
+
<p>Your Power Apps Code App is running. Open your AI assistant and describe what to build.</p>
|
|
517
|
+
</header>
|
|
518
|
+
<main className={styles.main}>
|
|
519
|
+
<div className={styles.card}>
|
|
520
|
+
<h2>🚀 Ready to build</h2>
|
|
521
|
+
<p>Open Copilot Chat or Claude Code and paste a prompt from <code>prompts/starter.md</code></p>
|
|
522
|
+
</div>
|
|
523
|
+
</main>
|
|
524
|
+
</div>
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
`);
|
|
528
|
+
|
|
529
|
+
// ── src/App.module.css ────────────────────────────────────────────────────
|
|
530
|
+
write(path.join(dir, "src", "App.module.css"), `.app {
|
|
531
|
+
min-height: 100vh;
|
|
532
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
533
|
+
background: #f4f4f8;
|
|
534
|
+
color: #1a1a2e;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.header {
|
|
538
|
+
background: #742774;
|
|
539
|
+
color: white;
|
|
540
|
+
padding: 2rem;
|
|
541
|
+
text-align: center;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.header h1 {
|
|
545
|
+
margin: 0 0 0.5rem;
|
|
546
|
+
font-size: 2rem;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.header p {
|
|
550
|
+
margin: 0;
|
|
551
|
+
opacity: 0.85;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.main {
|
|
555
|
+
max-width: 800px;
|
|
556
|
+
margin: 2rem auto;
|
|
557
|
+
padding: 0 1rem;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.card {
|
|
561
|
+
background: white;
|
|
562
|
+
border-radius: 8px;
|
|
563
|
+
padding: 1.5rem 2rem;
|
|
564
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.card h2 { margin-top: 0; }
|
|
568
|
+
`);
|
|
569
|
+
|
|
570
|
+
// ── src/index.css ─────────────────────────────────────────────────────────
|
|
571
|
+
write(path.join(dir, "src", "index.css"), `*, *::before, *::after { box-sizing: border-box; }
|
|
572
|
+
body { margin: 0; }
|
|
573
|
+
`);
|
|
574
|
+
|
|
575
|
+
// ── src/components/ placeholder ───────────────────────────────────────────
|
|
576
|
+
write(path.join(dir, "src", "components", ".gitkeep"), "");
|
|
577
|
+
|
|
578
|
+
// ── instructions ─────────────────────────────────────────────────────────
|
|
579
|
+
write(path.join(dir, "instructions", "codeapp-instructions.md"), `# Code App Instructions
|
|
580
|
+
|
|
581
|
+
## Type: Power Apps Code App (Code-first web app)
|
|
582
|
+
## Project: ${projectName}
|
|
583
|
+
|
|
584
|
+
### Tech stack (already installed — DO NOT change)
|
|
585
|
+
- Vite ^8 as the dev server and bundler (npm start = vite)
|
|
586
|
+
- React 18 + TypeScript 5
|
|
587
|
+
- CSS Modules for styling (*.module.css files)
|
|
588
|
+
- NO react-scripts, NO Create React App, NO webpack
|
|
589
|
+
|
|
590
|
+
### Connecting to Power Platform data
|
|
591
|
+
The @microsoft/power-apps client library is installed globally (not in package.json)
|
|
592
|
+
because it includes a native addon that must be compiled separately.
|
|
593
|
+
To add Power Platform data to a component:
|
|
594
|
+
1. Run once: npm install -g @microsoft/power-apps
|
|
595
|
+
2. Run once: npx power-apps init (creates power.config.json)
|
|
596
|
+
3. Import in components: import { useData } from '@microsoft/power-apps'
|
|
597
|
+
|
|
598
|
+
### Coding rules
|
|
599
|
+
- All components are React functional components with TypeScript types
|
|
600
|
+
- CSS goes in *.module.css files — import as: import styles from './Component.module.css'
|
|
601
|
+
- New pages/screens = new .tsx files in src/components/
|
|
602
|
+
- No class components, no .then() chains (use async/await)
|
|
603
|
+
- Never touch vite.config.ts or tsconfig.json unless asked
|
|
604
|
+
|
|
605
|
+
### To publish to Power Platform
|
|
606
|
+
1. npm run build (creates dist/ folder)
|
|
607
|
+
2. npx power-apps push (uploads to your environment)
|
|
608
|
+
`);
|
|
609
|
+
|
|
610
|
+
// ── prompts/starter.md ────────────────────────────────────────────────────
|
|
611
|
+
write(path.join(dir, "prompts", "starter.md"), `# Starter Prompts for ${projectName}
|
|
612
|
+
|
|
613
|
+
Your app is already running. Open your AI assistant and copy one of these prompts.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Build the home screen
|
|
618
|
+
"I'm building a Power Apps Code App called ${projectName} using React and TypeScript with Vite.
|
|
619
|
+
Replace the placeholder home screen in src/App.tsx with a proper layout that has:
|
|
620
|
+
- A navigation bar at the top with the app name and a user avatar placeholder
|
|
621
|
+
- A main content area with cards showing [describe your main content]
|
|
622
|
+
- A clean, professional design using the existing CSS module pattern"
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Add a new page
|
|
627
|
+
"Add a new page called [PageName] to this React app.
|
|
628
|
+
Create src/components/[PageName].tsx with [describe what the page does].
|
|
629
|
+
Add a navigation link to it in App.tsx."
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## Add a data table
|
|
634
|
+
"Add a component called DataTable.tsx in src/components/ that displays a table of [describe your data].
|
|
635
|
+
Use mock data for now — I'll connect it to real data later.
|
|
636
|
+
Style it using a CSS module."
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Connect to Power Platform data
|
|
641
|
+
"I want to connect this app to Power Platform.
|
|
642
|
+
Walk me through:
|
|
643
|
+
1. Installing @microsoft/power-apps globally
|
|
644
|
+
2. Running npx power-apps init
|
|
645
|
+
3. Adding a useData hook to fetch records from a Dataverse table called [TableName]"
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## Fix a TypeScript error
|
|
650
|
+
"I got this TypeScript error in [filename]: [paste error]
|
|
651
|
+
Explain what it means and fix it."
|
|
652
|
+
`);
|
|
653
|
+
|
|
654
|
+
// ── run npm install automatically ─────────────────────────────────────────
|
|
655
|
+
log.info("Running npm install (this takes about 30 seconds)...");
|
|
656
|
+
const installed = run("npm install", dir);
|
|
657
|
+
if (installed) {
|
|
658
|
+
log.ok("npm install completed — all dependencies ready");
|
|
659
|
+
} else {
|
|
660
|
+
log.warn("npm install had issues — try running npm install manually inside the project folder");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
log.ok("Code App structure created — run npm start to launch the app");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ─── AI context file writer ───────────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
function writeAIContextFile(dir, projectName, appType, aiTool) {
|
|
669
|
+
const content = `# Project: ${projectName}
|
|
670
|
+
|
|
671
|
+
## What this project does
|
|
672
|
+
[Fill in a one-paragraph description of your app — what problem it solves, who uses it]
|
|
673
|
+
|
|
674
|
+
## App type
|
|
675
|
+
${appType} on Microsoft Power Platform
|
|
676
|
+
|
|
677
|
+
## AI assistant
|
|
678
|
+
${aiTool}
|
|
679
|
+
|
|
680
|
+
## Coding conventions
|
|
681
|
+
- Use TypeScript (not plain JavaScript) wherever possible
|
|
682
|
+
- Use async/await (not .then() chains)
|
|
683
|
+
- All functions should have clear names that describe what they do
|
|
684
|
+
- Add a short comment above any complex logic explaining what it does
|
|
685
|
+
|
|
686
|
+
## Power Platform context
|
|
687
|
+
- See the instructions/ folder for ${appType}-specific rules
|
|
688
|
+
- Environment URL is in the .env file (POWER_PLATFORM_URL)
|
|
689
|
+
- Use PAC CLI to push/pull changes — see README.md for commands
|
|
690
|
+
|
|
691
|
+
## What to do when you get an error
|
|
692
|
+
Tell the AI: "I got this error: [paste error here]. What does it mean and how do I fix it?"
|
|
693
|
+
`;
|
|
694
|
+
|
|
695
|
+
if (aiTool === "Claude Code") {
|
|
696
|
+
write(path.join(dir, "CLAUDE.md"), content);
|
|
697
|
+
log.ok("Created CLAUDE.md — Claude Code reads this automatically on every session");
|
|
698
|
+
} else {
|
|
699
|
+
fs.mkdirSync(path.join(dir, ".github"), { recursive: true });
|
|
700
|
+
write(path.join(dir, ".github", "copilot-instructions.md"), content);
|
|
701
|
+
log.ok("Created .github/copilot-instructions.md — Copilot reads this for context");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
async function main() {
|
|
708
|
+
console.log(`
|
|
709
|
+
${c.bold}${c.magenta}╔═══════════════════════════════════════════╗
|
|
710
|
+
║ create-powerapp ⚡ ║
|
|
711
|
+
║ Scaffold any Power App in one command ║
|
|
712
|
+
╚═══════════════════════════════════════════╝${c.reset}
|
|
713
|
+
|
|
714
|
+
Welcome! Answer a few questions and your project will be ready
|
|
715
|
+
to open in VS Code and start prompting your AI assistant.
|
|
716
|
+
`);
|
|
717
|
+
|
|
718
|
+
// ── 1. Project name ──────────────────────────────────────────────────────
|
|
719
|
+
log.step("Project name");
|
|
720
|
+
log.dim("This becomes your folder name and app name. Use letters, numbers, hyphens.");
|
|
721
|
+
let projectName = await prompt(" Project name: ");
|
|
722
|
+
if (!projectName) projectName = "my-powerapp";
|
|
723
|
+
const safeName = projectName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
724
|
+
|
|
725
|
+
// ── 2. App type ──────────────────────────────────────────────────────────
|
|
726
|
+
log.step("Which type of Power App are you building?");
|
|
727
|
+
const appTypeChoice = await choose("App type:", [
|
|
728
|
+
{ label: "Canvas App", desc: "— Design every screen pixel-by-pixel. Best for custom-branded employee tools & mobile apps" },
|
|
729
|
+
{ label: "Model-Driven App", desc: "— Database-first. Power Apps builds the UI from your data model. Best for CRM / record management" },
|
|
730
|
+
{ label: "PCF Component", desc: "— A reusable custom control (widget) to embed inside other Power Apps" },
|
|
731
|
+
{ label: "Power Pages", desc: "— A public-facing website connected to your Dataverse data" },
|
|
732
|
+
{ label: "Code App", desc: "— A full custom web app (React/TypeScript) hosted on Power Platform" },
|
|
733
|
+
]);
|
|
734
|
+
|
|
735
|
+
// ── 3. AI assistant ──────────────────────────────────────────────────────
|
|
736
|
+
log.step("Which AI assistant are you using?");
|
|
737
|
+
const aiChoice = await choose("AI assistant:", [
|
|
738
|
+
{ label: "GitHub Copilot", desc: "— You have a GitHub Copilot subscription or organisation license" },
|
|
739
|
+
{ label: "Claude Code", desc: "— You have an Anthropic API key or Claude Code license" },
|
|
740
|
+
]);
|
|
741
|
+
|
|
742
|
+
// ── 4. Power Platform environment URL ────────────────────────────────────
|
|
743
|
+
log.step("Power Platform environment URL (optional — press Enter to skip)");
|
|
744
|
+
log.dim("Find it at make.powerapps.com → Settings gear → Session details → Instance url");
|
|
745
|
+
log.dim("Example: https://yourcompany.crm.dynamics.com");
|
|
746
|
+
const envUrl = await prompt(" Environment URL: ");
|
|
747
|
+
|
|
748
|
+
// ── 5. Create folder ─────────────────────────────────────────────────────
|
|
749
|
+
log.step(`Creating project folder: ${safeName}/`);
|
|
750
|
+
const dir = path.join(process.cwd(), safeName);
|
|
751
|
+
|
|
752
|
+
if (fs.existsSync(dir)) {
|
|
753
|
+
const overwrite = await prompt(` Folder "${safeName}" already exists. Overwrite? (y/N): `);
|
|
754
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
755
|
+
log.warn("Aborted. Choose a different project name.");
|
|
756
|
+
process.exit(0);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
761
|
+
log.ok(`Folder created: ${dir}`);
|
|
762
|
+
|
|
763
|
+
// ── 6. Shared files ──────────────────────────────────────────────────────
|
|
764
|
+
log.step("Creating shared project files...");
|
|
765
|
+
|
|
766
|
+
write(path.join(dir, ".gitignore"), gitignoreContent());
|
|
767
|
+
log.ok("Created .gitignore");
|
|
768
|
+
|
|
769
|
+
write(path.join(dir, ".env"), envUrl
|
|
770
|
+
? envContent(projectName, appTypeChoice.label).replace("https://yourorg.crm.dynamics.com", envUrl)
|
|
771
|
+
: envContent(projectName, appTypeChoice.label)
|
|
772
|
+
);
|
|
773
|
+
log.ok("Created .env");
|
|
774
|
+
|
|
775
|
+
write(path.join(dir, "README.md"), readmeContent(projectName, appTypeChoice.label, aiChoice.label));
|
|
776
|
+
log.ok("Created README.md");
|
|
777
|
+
|
|
778
|
+
// ── 7. App-type scaffold ─────────────────────────────────────────────────
|
|
779
|
+
log.step(`Scaffolding ${appTypeChoice.label} files...`);
|
|
780
|
+
switch (appTypeChoice.label) {
|
|
781
|
+
case "Canvas App": scaffoldCanvas(dir, projectName, aiChoice.label); break;
|
|
782
|
+
case "Model-Driven App": scaffoldMDA(dir, projectName, aiChoice.label); break;
|
|
783
|
+
case "PCF Component": scaffoldPCF(dir, projectName, aiChoice.label); break;
|
|
784
|
+
case "Power Pages": scaffoldPowerPages(dir, projectName, aiChoice.label); break;
|
|
785
|
+
case "Code App": scaffoldCodeApp(dir, projectName, aiChoice.label); break;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ── 8. AI context file ───────────────────────────────────────────────────
|
|
789
|
+
log.step("Creating AI context file...");
|
|
790
|
+
writeAIContextFile(dir, projectName, appTypeChoice.label, aiChoice.label);
|
|
791
|
+
|
|
792
|
+
// ── 9. Git init ──────────────────────────────────────────────────────────
|
|
793
|
+
log.step("Initialising Git...");
|
|
794
|
+
if (has("git")) {
|
|
795
|
+
run("git init", dir, true);
|
|
796
|
+
run("git add .", dir, true);
|
|
797
|
+
run('git commit -m "Initial scaffold by create-powerapp"', dir, true);
|
|
798
|
+
log.ok("Git initialised with first commit");
|
|
799
|
+
} else {
|
|
800
|
+
log.warn("Git not found — skipping. Install Git and run git init inside your project.");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ── 10. PAC CLI auth ─────────────────────────────────────────────────────
|
|
804
|
+
if (envUrl) {
|
|
805
|
+
log.step("Connecting to Power Platform environment...");
|
|
806
|
+
if (has("pac")) {
|
|
807
|
+
log.info(`Running: pac auth create --url ${envUrl}`);
|
|
808
|
+
log.dim("A browser window will open. Sign in with your Microsoft work account.");
|
|
809
|
+
run(`pac auth create --url ${envUrl}`, dir);
|
|
810
|
+
} else {
|
|
811
|
+
log.warn("PAC CLI not found. Install it with: npm install -g pac");
|
|
812
|
+
log.warn("Then run: pac auth create --url " + envUrl);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ── 11. Open in VS Code ──────────────────────────────────────────────────
|
|
817
|
+
log.step("Opening in VS Code...");
|
|
818
|
+
if (has("code")) {
|
|
819
|
+
run(`code "${dir}"`, process.cwd(), true);
|
|
820
|
+
log.ok("VS Code opened with your project");
|
|
821
|
+
} else {
|
|
822
|
+
log.warn("VS Code code command not found.");
|
|
823
|
+
log.dim("Open VS Code manually → File → Open Folder → select: " + dir);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ── Done ─────────────────────────────────────────────────────────────────
|
|
827
|
+
const isCodeApp = appTypeChoice.label === "Code App";
|
|
828
|
+
console.log(`
|
|
829
|
+
${c.bold}${c.green}╔═══════════════════════════════════════════════════╗
|
|
830
|
+
║ ✅ Your project is ready! ║
|
|
831
|
+
╚═══════════════════════════════════════════════════╝${c.reset}
|
|
832
|
+
|
|
833
|
+
${c.bold}Project folder:${c.reset} ${dir}
|
|
834
|
+
|
|
835
|
+
${c.bold}What to do next:${c.reset}
|
|
836
|
+
|
|
837
|
+
1. VS Code is now open (or open it manually and open the folder above)
|
|
838
|
+
${isCodeApp ? `
|
|
839
|
+
2. ${c.bold}${c.green}Start the app:${c.reset} In the VS Code terminal, run:
|
|
840
|
+
${c.cyan}npm start${c.reset}
|
|
841
|
+
Your browser will open automatically at http://localhost:3000
|
|
842
|
+
` : `
|
|
843
|
+
2. Read the ${c.cyan}prompts/starter.md${c.reset} file for ready-made prompts to get started
|
|
844
|
+
`}
|
|
845
|
+
3. Open your AI assistant:
|
|
846
|
+
${aiChoice.label === "Claude Code"
|
|
847
|
+
? ` • In the VS Code terminal, run: ${c.cyan}claude${c.reset}
|
|
848
|
+
• Or click the Claude Code icon in the left sidebar`
|
|
849
|
+
: ` • Click the Copilot Chat icon (💬) in the VS Code left sidebar`}
|
|
850
|
+
|
|
851
|
+
4. Copy a prompt from ${c.cyan}prompts/starter.md${c.reset} and paste it in — or describe your app in your own words
|
|
852
|
+
|
|
853
|
+
5. When you're happy with what the AI built, save your work:
|
|
854
|
+
${c.dim}git add . → git commit -m "describe what you built" → git push${c.reset}
|
|
855
|
+
|
|
856
|
+
${c.bold}${c.yellow}Tip:${c.reset} When something goes wrong, tell the AI:
|
|
857
|
+
"I got this error: [paste error]. What does it mean and how do I fix it?"
|
|
858
|
+
|
|
859
|
+
Happy building! 🚀
|
|
860
|
+
`);
|
|
861
|
+
rl.close();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
main().catch((err) => {
|
|
865
|
+
log.error("Unexpected error: " + err.message);
|
|
866
|
+
rl.close();
|
|
867
|
+
process.exit(1);
|
|
868
|
+
});
|