Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/reference/copilot-plugin.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
# Install and use the Copilot plugin

The SQL Apps plugin adds project-specific skills to GitHub Copilot. It does not require publication, a Git push, an Azure subscription, or another platform plugin. Copilot itself still requires its normal account/access and network connectivity; the local application does not call cloud APIs after prerequisite downloads.
The SQL Apps plugin adds project-specific skills to GitHub Copilot. Installing its skills and launcher does not require npm publication, an Azure subscription, or another platform plugin. Copilot itself still requires its normal account/access and network connectivity; the local application does not call cloud APIs after prerequisite downloads.

## Install from GitHub

Register the repository marketplace and install the dedicated plugin:

```powershell
copilot plugin marketplace add microsoft/sql-apps
copilot plugin install sql-apps@sql-apps-skills
```

This installs the manifest, six skills and launcher, not the application runtime or project factory. A listing in the default Copilot marketplace requires separate upstream review; registering this repository does not create that listing.

First-project creation still needs a foundation checkout. Select it explicitly in the terminal used to launch Copilot, without replacing a remote marketplace registration with the local installer:

```powershell
git clone https://github.com/microsoft/sql-apps.git "C:\git\sql-apps"
$env:SQL_APPS_HOME = "C:\git\sql-apps"
copilot
```

Use an existing foundation checkout instead of cloning again when appropriate. `SQL_APPS_HOME` takes precedence for project creation when it names the foundation; a generated application is not a project factory. The existing local installer below remains an alternative for checkout-based development and creates a persistent foundation binding.

Generated applications restore exactly `@microsoft/sql-apps@0.2.3` from the configured npm feed. Plugin installation does not publish that package or prove it is available. A missing package blocks application restoration; a local tarball or foundation build is not clean-feed release acceptance. See the [release prerequisites](progressive-creation.md).

## Install from your checkout

Expand Down
15 changes: 4 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions plugins/sql-apps/scripts/sql-apps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export async function resolveHome(options = {}) {

async function resolveToolHome(options = {}) {
const environment = options.environment ?? process.env;
if (environment.SQL_APPS_HOME !== undefined) {
const home = await validateHome(environment.SQL_APPS_HOME);
const pkg = JSON.parse(await readFile(join(home, 'package.json'), 'utf8'));
if (pkg.name === 'sql-apps') return home;
}
const userHome = resolve(options.userHome ?? homedir());
const configuredBinding = join(environment.COPILOT_HOME || join(userHome, '.copilot'), 'sql-apps-local.json');
let binding;
Expand Down
6 changes: 3 additions & 3 deletions scripts/check-application.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { readFile, readdir, lstat } from 'node:fs/promises';
import { join, resolve, extname, relative, dirname, basename } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import ts from 'typescript';

const sampleMarkers = [
/\b(?:TodoExampleClient|TodoOwnerPredicate|TodoOwnerPolicy)\b/,
Expand Down Expand Up @@ -242,7 +241,8 @@ async function customizedUi(root) {
new Set(['app.js', 'auth/redirect.js']));
}

function exampleDependencies(path, content) {
async function exampleDependencies(path, content) {
const { default: ts } = await import('typescript');
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
const dependencies = new Set();
const scopes = [new Map()];
Expand Down Expand Up @@ -316,7 +316,7 @@ export async function checkApplication(root = process.cwd(), model) {
inspected++;
const content = await readFile(path, 'utf8');
if (tests) {
for (const dependency of exampleDependencies(path, content)) {
for (const dependency of await exampleDependencies(path, content)) {
violations.push(`Unselected example dependency in ${path}: ${dependency}`);
}
} else if (sampleMarkers.some(marker => marker.test(content))) violations.push(path);
Expand Down
9 changes: 7 additions & 2 deletions tests/creation-front-door.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ test('one origin serves pre-install status, verified app, proxy and isolated fai
const root = await realpath(await mkdtemp(join(tmpdir(), 'sql-apps-front-door-')));
let door, upstream, releaseSlowCheck, slowCheckStarted;
const slowCheckStartedPromise = new Promise(done => { slowCheckStarted = done; });
const slowCheckReleased = new Promise(done => { releaseSlowCheck = done; });
t.after(async () => {
releaseSlowCheck?.();
if (door) await door.close();
Expand Down Expand Up @@ -95,7 +96,8 @@ test('one origin serves pre-install status, verified app, proxy and isolated fai
} else if (request.url === '/api/Reports') {
response.writeHead(503).end('{"error":"Fixture report unavailable"}');
} else if (request.url === '/api/Slow') {
await new Promise(done => { releaseSlowCheck = done; slowCheckStarted(); });
slowCheckStarted();
await slowCheckReleased;
response.end('{"value":[{"ready":1}]}');
} else if (request.url === '/api/Fast') {
response.end('{"value":[{"ready":1}]}');
Expand All @@ -121,6 +123,8 @@ test('one origin serves pre-install status, verified app, proxy and isolated fai
const usable = await eventually(publicOrigin, state => state.usable);
assert.equal(usable.ready, false);
assert.deepEqual(usable.blockers, ['browser-workflow', 'design-review']);
const completed = await (await fetch(`${publicOrigin}/__sql_apps/check`)).json();
assert.equal(completed.usable, true);
await new Promise(done => setTimeout(done, 225));
const cached = await (await fetch(`${publicOrigin}/__sql_apps/progress`)).json();
assert.equal(cached.usable, false, 'an idle cached observation must not claim current health');
Expand Down Expand Up @@ -165,7 +169,8 @@ test('one origin serves pre-install status, verified app, proxy and isolated fai
valid = false;
await eventually(publicOrigin, state => state.capabilities.some(item => item.state === 'failed'));
healthy = false;
const dataAccessFailure = await eventually(publicOrigin, state => state.diagnostic === 'DATA_ACCESS_NOT_READY');
const dataAccessFailure = await eventually(publicOrigin, state => state.diagnostic === 'DATA_ACCESS_NOT_READY' &&
state.capabilities.find(item => item.id === 'data').state === 'verified');
assert.equal(dataAccessFailure.capabilities.find(item => item.id === 'data').state, 'verified');
assert.match(await (await fetch(publicOrigin)).text(), /Bringing your application to life/);
healthy = true; valid = true;
Expand Down
22 changes: 20 additions & 2 deletions tests/creation-process.test.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { mkdir, mkdtemp, rm, cp } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { pathToFileURL } from 'node:url';
import { tmpdir } from 'node:os';
import { initialProgress } from '../src/creation-progress.mjs';
import { readCreationJson, writeCreationJson } from '../src/creation-session.mjs';
import { startCreationProcess } from '../scripts/creation.mjs';

test('cold creation and design evidence helpers load without restored packages or build output', async t => {
const root = await mkdtemp(join(tmpdir(), 'sql-apps-cold-creation-'));
t.after(() => rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }));
for (const directory of ['src', 'scripts']) {
await cp(resolve(directory), join(root, directory), { recursive: true });
}
const module = pathToFileURL(join(root, 'scripts', 'creation.mjs')).href;
const check = pathToFileURL(join(root, 'scripts', 'check-application.mjs')).href;
const { stdout } = await promisify(execFile)(process.execPath, ['--input-type=module', '-e',
`await import(${JSON.stringify(module)}); const { inspectDesignAcceptance } = await import(${JSON.stringify(check)}); ` +
`console.log(JSON.stringify(await inspectDesignAcceptance(process.cwd())));`],
{ cwd: root });
assert.equal(JSON.parse(stdout).accepted, false);
});

async function freeOrigin() {
const server = createServer();
await new Promise(done => server.listen(0, '127.0.0.1', done));
Expand Down
15 changes: 1 addition & 14 deletions tests/local-consent.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const execute = promisify(execFile);
const cli = resolve('scripts/local-consent.mjs');

async function fixture(t, selected = false) {
const home = await mkdtemp(join(tmpdir(), 'sql-apps-consent-'));
const home = await realpath(await mkdtemp(join(tmpdir(), 'sql-apps-consent-')));
t.after(() => rm(home, { recursive: true, force: true }));
const files = {
'package.json': JSON.stringify({ name: 'sql-apps', scripts: { build: 'node scripts/build-provenance.mjs' } }),
Expand Down Expand Up @@ -105,19 +105,6 @@ test('Express instance changes are separate, target-bound one-operation grants,
await assert.rejects(assertConsent(home, authRequest), /permission/);
});

test('endpoint CLI initializes its approved workspace without interpreting the port block as a container', async t => {
const home = await realpath(await fixture(t));
const { proposeWorkspace } = await import('../src/workspace.mjs');
const proposal = await proposeWorkspace(home, undefined, { includeSql: false });
const workspace = String((proposal.conflicts.length ? proposal.alternative : proposal.descriptor).basePort);
const sqlTarget = { version: 1, provider: 'express', instance: 'SQLEXPRESS', server: '127.0.0.1', port: 1433, database: 'development' };
const bundle = await planBundle(home, { profile: 'role-based-data', workspace, sqlTarget });
await grantBundle(home, { profile: 'role-based-data', workspace, sqlTarget, approval: bundle.approval });
await execute(process.execPath, [resolve('dist', 'src', 'local-cli.js'), 'workspace-init', workspace],
{ cwd: home, env: { ...process.env, SQL_APPS_PROJECT_HOME: home } });
assert.equal(JSON.parse(await readFile(join(home, '.sql-apps', 'workspace.json'), 'utf8')).basePort, Number(workspace));
});

test('composite startup lists every missing scope before any mutation', async t => {
const home = await fixture(t, true);
const plan = await planConsent(home, { operation: 'role-based-app' });
Expand Down
34 changes: 32 additions & 2 deletions tests/local.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { cp, readFile, realpath, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { localCommand, localConnection, localDabConfig, localPrincipal, verifyLocal, startLocalSql, sqlImage, dabImage } from '../src/local.js';
import { run, type Run } from '../src/process.js';
import { startLocalApplication } from '../src/local-startup.js';
import { runtimeFor, type WorkspaceRuntime } from '../src/workspace.mjs';
import { proposeWorkspace, runtimeFor, type WorkspaceRuntime } from '../src/workspace.mjs';
import { planBundle, grantBundle } from '../src/local-consent.mjs';
import { localWorkspaceFixture } from './local-workspace-fixture.js';

function sqlOwnership(runtime: WorkspaceRuntime, args: readonly string[]): string | undefined {
Expand Down Expand Up @@ -62,6 +64,34 @@ test('actual application CLI rejects invalid input during preflight before start
/Invalid SQL container name/.test(error.message) && !/Starting SQL container/.test(error.message));
});

test('endpoint CLI initializes its approved workspace without interpreting the port block as a container', async t => {
const source = process.cwd();
await localWorkspaceFixture(t);
const home = await realpath(process.cwd());
for (const directory of ['src', 'scripts', 'sql', 'dab', '.config']) {
await cp(join(source, directory), join(home, directory), {
recursive: true,
filter: path => !/(?:^|[\\/])(?:bin|obj|node_modules)(?:[\\/]|$)/.test(path),
});
}
for (const file of ['package.json', 'package-lock.json', 'tsconfig.json', 'tsconfig.runtime.json']) {
await cp(join(source, file), join(home, file));
}
await writeFile(join(home, 'role-based-data.json'),
'{"profile":"role-based-data","requiredRole":"AppUser","readinessPath":"/api/Ready"}');
const proposal = await proposeWorkspace(home, undefined, { includeSql: false });
const selection = proposal.conflicts?.length ? proposal.alternative : proposal.descriptor;
assert.ok(selection, 'A free workspace port block must be available');
const workspace = String(selection.basePort);
const sqlTarget = { version: 1, provider: 'express', instance: 'SQLEXPRESS', server: '127.0.0.1',
port: 1433, database: 'development' } as const;
const bundle = await planBundle(home, { profile: 'role-based-data', workspace, sqlTarget });
await grantBundle(home, { profile: 'role-based-data', workspace, sqlTarget, approval: bundle.approval });
await run(process.execPath, [resolve(source, 'dist', 'src', 'local-cli.js'), 'workspace-init', workspace],
{ env: { SQL_APPS_PROJECT_HOME: home } });
assert.equal(JSON.parse(await readFile(join(home, '.sql-apps', 'workspace.json'), 'utf8')).basePort, Number(workspace));
});

test('local workflow fails closed on SQL Server instead of Azure SQL', async () => {
await verifyLocal('sqldbdev', async () => '5|SQL Azure\r\n');
for (const result of ['3|Developer Edition', '', '5|unrecognized']) {
Expand Down
15 changes: 14 additions & 1 deletion tests/projects.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ test('a fresh generated project installs its exact tooling version with npm ci f

test('create scaffolds an independent named project and records it as active', async t => {
const source = await sourceFixture(t);
const userHome = await mkdtemp(join(tmpdir(), 'sql-apps-project-user-'));
const userHome = await realpath(await mkdtemp(join(tmpdir(), 'sql-apps-project-user-')));
t.after(() => rm(userHome, { recursive: true, force: true }));
const created = await projects.createProject(source, 'To Do', { userHome, initializeGit: false });
assert.equal(created.name, 'To Do');
Expand Down Expand Up @@ -411,3 +411,16 @@ test('plugin project dispatch uses the bound foundation as scaffold source witho
});
assert.deepEqual(calls, [{ args: ['create', 'A New App'], home: source }]);
});

test('marketplace project dispatch accepts an explicit foundation without a local-plugin binding', async t => {
const source = await sourceFixture(t);
const userHome = await mkdtemp(join(tmpdir(), 'sql-apps-marketplace-project-'));
t.after(() => rm(userHome, { recursive: true, force: true }));
const calls = [];
await plugin.dispatchProjects(['create-plan', 'A New App'], {
userHome,
environment: { SQL_APPS_HOME: source },
projectMain: async (args, home) => calls.push({ args, home }),
});
assert.deepEqual(calls, [{ args: ['create-plan', 'A New App'], home: source }]);
});
6 changes: 6 additions & 0 deletions tests/tooling-package.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ test('tooling:pack uses a POSIX-portable package path', async () => {
);
});

test('locked tooling dependencies exclude the unpatched sprintf-js advisory', async () => {
const lock = JSON.parse(await readFile(join(root, 'package-lock.json'), 'utf8'));
assert.equal(Object.keys(lock.packages).some(path => /(?:^|\/)node_modules\/sprintf-js$/.test(path)), false,
'Use the upstream tedious fix instead of shipping CVE-2026-97058');
});

test('stages publishable package metadata with synchronized exact runtime dependencies', async () => {
const [pkg, source, rootPackage] = await Promise.all([
readFile(join(staged, 'package.json'), 'utf8').then(JSON.parse),
Expand Down
Loading