Skip to content

Commit 625e2fe

Browse files
nguyentony95Tony Nguyen
andauthored
Fix/model apps update check (#409)
* feat(model-apps): add plugin update notices Adopt the Power Pages version-check preflight across all user-invocable Model Apps skills, with host-specific commands for GitHub Copilot CLI and Claude Code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8b26c0b1-830d-4b5b-b070-76238732b56f * chore(model-apps): preserve telemetry skill line endings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8b26c0b1-830d-4b5b-b070-76238732b56f --------- Co-authored-by: Tony Nguyen <nguyentony@microsoft.com> Copilot-Session: 8b26c0b1-830d-4b5b-b070-76238732b56f
1 parent 50457bb commit 625e2fe

10 files changed

Lines changed: 246 additions & 6 deletions

File tree

plugins/model-apps/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "model-apps",
3-
"version": "2.4.3",
3+
"version": "2.4.4",
44
"description": "Build model-driven Power Apps end to end: /app-builder authors whole apps (tables, relationships, forms, views, charts, security roles, app + sitemap) from a natural-language intent, and /genpage builds generative pages with specialist agents for planning, entity creation, and parallel code generation. Requires PAC CLI > 2.10.0 and Azure CLI (`az`). See CHANGELOG.md for v1.x -> v2.x migration.",
55
"author": {
66
"name": "Microsoft",

plugins/model-apps/.plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "model-apps",
3-
"version": "2.4.3",
3+
"version": "2.4.4",
44
"description": "Build model-driven Power Apps end to end: /app-builder authors whole apps (tables, relationships, forms, views, charts, security roles, app + sitemap) from a natural-language intent, and /genpage builds generative pages with specialist agents for planning, entity creation, and parallel code generation. Requires PAC CLI > 2.10.0 and Azure CLI (`az`). See CHANGELOG.md for v1.x -> v2.x migration.",
55
"author": {
66
"name": "Microsoft",

plugins/model-apps/AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,9 @@ repo-root `shared/telemetry/`; `scripts/lib/telemetry/lib` is a **physical copy*
687687
- Write descriptions in third person ("Creates X" not "This skill guides you through creating X")
688688
- Use progressive disclosure: SKILL.md for workflow, reference files for details
689689
- Link to references inline: `See [troubleshooting.md](../../references/troubleshooting.md)`
690+
- Immediately after the frontmatter of every user-invocable skill, run
691+
`node "${PLUGIN_ROOT}/scripts/check-version.js"` and show any output before
692+
proceeding. The check is best-effort and must never block skill execution.
690693

691694
## Building & Testing
692695

plugins/model-apps/CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
All notable changes to the **model-apps** plugin.
44

5-
## [Unreleased] — 2.4.3
5+
## [Unreleased] — 2.4.4
66

7-
Fixes four crash paths and a smoke-eval assertion that could never pass live.
7+
Adds plugin update notices, fixes four crash paths, and corrects a smoke-eval
8+
assertion that could never pass live.
9+
10+
### Added
11+
- **Automatic plugin update notice.** Every user-invocable skill now runs the
12+
non-blocking `scripts/check-version.js` preflight, which compares the installed
13+
Model Apps version with `origin/main` and shows update commands for the active
14+
GitHub Copilot CLI or Claude Code host when a newer version is available.
815

916
### Fixed
1017
- **Malformed specs now produce validation errors instead of raw `TypeError`s.** `validateAppSpec()`
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Plugin version check. Compares the local plugin manifest version against
5+
* origin/main and prints an update notice if the remote version is newer.
6+
* Exits silently if versions match or on any error.
7+
*
8+
* Usage: node check-version.js
9+
* Functions are also exported for testing.
10+
*/
11+
12+
const { execFileSync, execSync } = require('child_process');
13+
const path = require('path');
14+
const fs = require('fs');
15+
16+
const MARKETPLACE_PATHS = [
17+
'marketplace.json',
18+
'.plugin/marketplace.json',
19+
'.claude-plugin/marketplace.json',
20+
];
21+
const PLUGIN_MANIFEST_PATHS = [
22+
'.plugin/plugin.json',
23+
'.claude-plugin/plugin.json',
24+
];
25+
26+
function compareSemver(localVersion, remoteVersion) {
27+
const localParts = localVersion.split('.').map(Number);
28+
const remoteParts = remoteVersion.split('.').map(Number);
29+
for (let index = 0; index < 3; index++) {
30+
if ((remoteParts[index] || 0) > (localParts[index] || 0)) return 1;
31+
if ((remoteParts[index] || 0) < (localParts[index] || 0)) return -1;
32+
}
33+
return 0;
34+
}
35+
36+
function detectHost(env = process.env) {
37+
return env.COPILOT_CLI === '1' ? 'copilot' : 'claude';
38+
}
39+
40+
function formatUpdateMessage(
41+
pluginName,
42+
localVersion,
43+
remoteVersion,
44+
marketplaceName,
45+
host = detectHost()
46+
) {
47+
const qualifiedName = marketplaceName ? `${pluginName}@${marketplaceName}` : pluginName;
48+
let message = `\nPlugin update available: ${pluginName} ${localVersion} -> ${remoteVersion}.\n`;
49+
if (marketplaceName) {
50+
message += `Run:\n ${host} plugin marketplace update ${marketplaceName}\n ${host} plugin update ${qualifiedName}`;
51+
} else {
52+
message += `Run: ${host} plugin update ${qualifiedName}`;
53+
}
54+
return message;
55+
}
56+
57+
function firstExistingPath(root, relativePaths) {
58+
for (const relativePath of relativePaths) {
59+
const filePath = path.join(root, relativePath);
60+
if (fs.existsSync(filePath)) return filePath;
61+
}
62+
return null;
63+
}
64+
65+
function readFirstJson(root, relativePaths) {
66+
const filePath = firstExistingPath(root, relativePaths);
67+
if (!filePath) return null;
68+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
69+
}
70+
71+
function readMarketplaceName(gitRoot) {
72+
const marketplace = readFirstJson(gitRoot, MARKETPLACE_PATHS);
73+
return marketplace?.name || null;
74+
}
75+
76+
function readJsonFromGit(ref, relativePaths) {
77+
for (const relativePath of relativePaths) {
78+
try {
79+
const content = execFileSync('git', ['show', `${ref}:${relativePath}`], {
80+
encoding: 'utf8',
81+
timeout: 5000,
82+
stdio: ['pipe', 'pipe', 'pipe'],
83+
});
84+
return JSON.parse(content);
85+
} catch {
86+
// Open Plugins and legacy installs use different manifest paths.
87+
}
88+
}
89+
return null;
90+
}
91+
92+
module.exports = { compareSemver, detectHost, formatUpdateMessage, readMarketplaceName };
93+
94+
if (require.main === module) {
95+
try {
96+
const pluginRoot = path.resolve(__dirname, '..');
97+
const pluginJsonPath = firstExistingPath(pluginRoot, PLUGIN_MANIFEST_PATHS);
98+
if (!pluginJsonPath) process.exit(0);
99+
100+
const localPlugin = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
101+
const localVersion = localPlugin.version;
102+
if (!localVersion) process.exit(0);
103+
104+
const gitRoot = execSync('git rev-parse --show-toplevel', {
105+
encoding: 'utf8',
106+
timeout: 5000,
107+
stdio: ['pipe', 'pipe', 'pipe'],
108+
}).trim();
109+
110+
const remoteManifestPaths = PLUGIN_MANIFEST_PATHS.map((manifestPath) =>
111+
path.relative(gitRoot, path.join(pluginRoot, manifestPath)).replace(/\\/g, '/')
112+
);
113+
114+
try {
115+
execSync('git fetch origin main --quiet', {
116+
encoding: 'utf8',
117+
timeout: 10000,
118+
stdio: ['pipe', 'pipe', 'pipe'],
119+
});
120+
} catch {
121+
// A cached origin/main is sufficient when the network is unavailable.
122+
}
123+
124+
const remotePlugin = readJsonFromGit('origin/main', remoteManifestPaths);
125+
if (!remotePlugin?.version) process.exit(0);
126+
127+
if (compareSemver(localVersion, remotePlugin.version) > 0) {
128+
const pluginName = localPlugin.name || 'model-apps';
129+
const marketplaceName = readMarketplaceName(gitRoot);
130+
console.log(
131+
formatUpdateMessage(pluginName, localVersion, remotePlugin.version, marketplaceName)
132+
);
133+
}
134+
} catch {
135+
// Version checks must never block skill execution.
136+
}
137+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert/strict');
3+
const fs = require('node:fs');
4+
const os = require('node:os');
5+
const path = require('node:path');
6+
7+
const {
8+
compareSemver,
9+
detectHost,
10+
formatUpdateMessage,
11+
readMarketplaceName,
12+
} = require('../check-version');
13+
14+
test('compareSemver compares major, minor, and patch versions', () => {
15+
assert.equal(compareSemver('1.2.0', '1.2.0'), 0);
16+
assert.equal(compareSemver('1.2', '1.2.0'), 0);
17+
assert.equal(compareSemver('1.2.0', '2.0.0'), 1);
18+
assert.equal(compareSemver('1.2.0', '1.3.0'), 1);
19+
assert.equal(compareSemver('1.2.0', '1.2.1'), 1);
20+
assert.equal(compareSemver('2.0.0', '1.9.9'), -1);
21+
});
22+
23+
test('detectHost recognizes GitHub Copilot CLI', () => {
24+
assert.equal(detectHost({ COPILOT_CLI: '1' }), 'copilot');
25+
assert.equal(detectHost({}), 'claude');
26+
});
27+
28+
test('formatUpdateMessage emits Copilot update commands', () => {
29+
const message = formatUpdateMessage(
30+
'model-apps',
31+
'2.4.3',
32+
'2.4.4',
33+
'power-platform-skills',
34+
'copilot'
35+
);
36+
37+
assert.match(message, /model-apps 2\.4\.3 -> 2\.4\.4/);
38+
assert.match(message, /copilot plugin marketplace update power-platform-skills/);
39+
assert.match(message, /copilot plugin update model-apps@power-platform-skills/);
40+
assert.ok(message.indexOf('marketplace update') < message.indexOf('plugin update model-apps@'));
41+
});
42+
43+
test('formatUpdateMessage emits Claude update commands', () => {
44+
const message = formatUpdateMessage(
45+
'model-apps',
46+
'2.4.3',
47+
'2.4.4',
48+
'power-platform-skills',
49+
'claude'
50+
);
51+
52+
assert.match(message, /claude plugin marketplace update power-platform-skills/);
53+
assert.match(message, /claude plugin update model-apps@power-platform-skills/);
54+
});
55+
56+
test('formatUpdateMessage uses the plain plugin name without a marketplace', () => {
57+
const message = formatUpdateMessage('model-apps', '2.4.3', '2.4.4', null, 'copilot');
58+
59+
assert.match(message, /copilot plugin update model-apps/);
60+
assert.doesNotMatch(message, /marketplace update/);
61+
assert.doesNotMatch(message, /@/);
62+
});
63+
64+
test('readMarketplaceName reads the repository marketplace', () => {
65+
const { execSync } = require('node:child_process');
66+
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
67+
68+
assert.equal(readMarketplaceName(gitRoot), 'power-platform-skills');
69+
});
70+
71+
test('readMarketplaceName falls back to the legacy marketplace path', () => {
72+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'model-apps-version-'));
73+
fs.mkdirSync(path.join(tempDir, '.claude-plugin'));
74+
fs.writeFileSync(
75+
path.join(tempDir, '.claude-plugin', 'marketplace.json'),
76+
JSON.stringify({ name: 'legacy-marketplace' })
77+
);
78+
79+
assert.equal(readMarketplaceName(tempDir), 'legacy-marketplace');
80+
});
81+
82+
test('readMarketplaceName returns null when no marketplace exists', () => {
83+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'model-apps-version-'));
84+
assert.equal(readMarketplaceName(tempDir), null);
85+
});

plugins/model-apps/skills/app-builder/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
---
22
name: app-builder
3-
version: 0.8.0
3+
version: 0.8.1
44
description: (Preview) Builds and edits a model-driven Power Apps app from a natural-language intent — tables, columns, relationships, adaptive forms with sub-grids, views, Choice-column charts, generative page intents for overview/dashboard surfaces (page `.tsx` generated in generate-pages after plan approval), and an app module + sitemap — via the headless cds-maker-sdk. Runs an interactive, multi-turn authoring flow (env selection, jobs-to-be-done first, then design-only App Spec authoring across confirmed levels, guardrail lint, plan-mode approval, generate-pages, full build) and a narrated build, and can download a deployed app back into an editable spec to change it. Use when the user says "build an app for X", "create a model-driven app", "make me an app to manage Y", or "edit/add to my app". This skill stands alone and does not require /genpage — but for a standalone generative page added to an app that already exists, use /genpage instead.
55
author: Microsoft Corporation
66
argument-hint: "<app description>"
77
user-invocable: true
88
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, AskUserQuestion, EnterPlanMode, ExitPlanMode, TaskCreate, TaskUpdate, TaskList
99
---
1010

11+
> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
12+
1113
# app-builder — intent → model-driven app
1214

1315
> ⚠️ **Preview.** This skill is in preview — its App Spec shape, flags, and build behavior may change

plugins/model-apps/skills/genpage/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: genpage
3-
version: 2.3.0
3+
version: 2.3.1
44
description: Creates, updates, and deploys Power Apps generative pages for model-driven apps using React v17, TypeScript, and Fluent UI V9. Orchestrates specialist agents for planning, entity creation, and code generation. Use it when user asks to build, retrieve, or update a page in an existing Microsoft Power Apps model-driven app. Use it when user mentions "generative page", "page in a model-driven", or "genux". This skill stands alone and does not require /app-builder — but if the user wants a whole app built (tables, forms, views, sitemap) rather than pages for an app that already exists, use /app-builder instead.
55
author: Microsoft Corporation
66
argument-hint: "<page description> | edit"
@@ -9,6 +9,8 @@ model: sonnet
99
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, Task, AskUserQuestion, TaskCreate, TaskUpdate, TaskList
1010
---
1111

12+
> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
13+
1214
# Power Apps Generative Pages Builder
1315

1416
**Triggers:** genpage, generative page, create genpage, genux page, build genux, power apps page, model page

plugins/model-apps/skills/report-issue/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskUpdate,
1010
model: sonnet
1111
---
1212

13+
> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
14+
1315
**Workflow: [report-issue-workflow.md](${PLUGIN_ROOT}/skills/report-issue/report-issue-workflow.md)** — Read and follow all phases defined in that bundled file.

plugins/model-apps/skills/telemetry/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ allowed-tools: Bash
1111
model: haiku
1212
---
1313

14+
> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
15+
1416
**Workflow: [telemetry-workflow.md](${PLUGIN_ROOT}/skills/telemetry/telemetry-workflow.md)** — Read and follow all steps defined in that bundled file.

0 commit comments

Comments
 (0)