Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0480877
docs: add design spec for desktop terminal-action bridge
ElfredSeow Jul 13, 2026
3a1269c
docs: add implementation plan for desktop terminal-action bridge
ElfredSeow Jul 13, 2026
79d10f9
feat(lifecycle): add buildAndPush β€” npm run build then pac code push
ElfredSeow Jul 13, 2026
4be50e2
chore: ignore .superpowers/ scratch dir; normalize package-lock.json
ElfredSeow Jul 13, 2026
c8cd359
fix(lifecycle): buildAndPush never rejects, even when push throws
ElfredSeow Jul 13, 2026
cf29bdc
feat(lifecycle): add datasource.js β€” pac code add-data-source wrapper
ElfredSeow Jul 13, 2026
ea091e9
test(lifecycle): cover addDataSource's pac-unreachable and CLI-reject…
ElfredSeow Jul 13, 2026
83426c0
feat(lifecycle): add scaffold-cli.js β€” spawn create-powercodex.js for…
ElfredSeow Jul 13, 2026
9f6f2c6
fix(lifecycle): scaffold-cli sanitizes name and tests real binPath() …
ElfredSeow Jul 13, 2026
4c4cd78
fix(lifecycle): scaffold-cli traversal test asserts path containment,…
ElfredSeow Jul 13, 2026
759dcf9
feat(lifecycle): classify push / add-datasource / scaffold-project ch…
ElfredSeow Jul 13, 2026
37d5b4b
fix(lifecycle): tighten push-intent regex to avoid misrouting non-dep…
ElfredSeow Jul 13, 2026
8d4dce6
fix(lifecycle): publish-intent regex matches bare "this"/"it" like pu…
ElfredSeow Jul 13, 2026
12f4552
feat(lifecycle): wire push and add-datasource into Controller.action()
ElfredSeow Jul 13, 2026
ac2d617
test(lifecycle): add-datasource gate has its own "blocked while off" …
ElfredSeow Jul 13, 2026
bf82fd5
feat(lifecycle): chat-driven push / add-datasource / scaffold-project…
ElfredSeow Jul 13, 2026
116aac9
fix(lifecycle): correct straight/curly quote handling in name/table e…
ElfredSeow Jul 13, 2026
231101e
fix(lifecycle): revert AGENT_SYSTEM apostrophes to original curly form
ElfredSeow Jul 13, 2026
30091bf
feat(lifecycle): wire scaffold-project routing + dataverse-state endp…
ElfredSeow Jul 13, 2026
411b374
fix(lifecycle): scaffold-project selftest fails fast, never spawns a …
ElfredSeow Jul 13, 2026
45e1c21
feat(chat): add Push, Add datasource, and New project to the toolbar
ElfredSeow Jul 13, 2026
cd57159
fix(chat): repair inline-script syntax error and escape user input re…
ElfredSeow Jul 13, 2026
3a45ec6
fix(desktop): vendor bin/create-powercodex.js + full templates/ so Ne…
ElfredSeow Jul 13, 2026
83478e2
fix(lifecycle): scaffold-project intent requires "new" adjacent to "p…
ElfredSeow Jul 13, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ Approved_rights/
**/.profiles/
# PowerCodex local build/compile scratch dirs
.tmp-*
.superpowers/
39 changes: 25 additions & 14 deletions desktop/scripts/sync-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,30 @@ fs.cpSync(src, dst, {

console.log('synced lifecycle β†’', path.relative(process.cwd(), dst));

// The published starter is the canonical scaffold for desktop "Create a new app"
// (decision D5). Vendor it beside the engine so desktop-born projects get the full
// harness / e2e / data layout; scaffold.js resolves it at vendor/templates/starter.
const tplSrc = path.resolve(__dirname, '..', '..', 'templates', 'starter');
const tplDst = path.resolve(__dirname, '..', 'vendor', 'templates', 'starter');
if (fs.existsSync(tplSrc)) {
fs.rmSync(tplDst, { recursive: true, force: true });
fs.mkdirSync(path.dirname(tplDst), { recursive: true });
fs.cpSync(tplSrc, tplDst, {
recursive: true,
filter: (s) => !SKIP.test(s),
});
console.log('synced starter β†’', path.relative(process.cwd(), tplDst));
// Vendor the whole templates/ tree (starter + github OPSX prompts/skills + the fixed
// openspec/config.yaml) and bin/create-powercodex.js, so "✨ New project" inside the
// packaged app can spawn the exact same full scaffold the `powercodex` CLI produces
// (one CLI, two entry points β€” decision D5, extended). scaffold-cli.js resolves the
// vendored bin at vendor/bin/create-powercodex.js, which in turn resolves its own
// template root at vendor/templates/ relative to itself β€” no path changes needed
// inside create-powercodex.js itself.
const templatesSrc = path.resolve(__dirname, '..', '..', 'templates');
const templatesDst = path.resolve(__dirname, '..', 'vendor', 'templates');
if (fs.existsSync(templatesSrc)) {
fs.rmSync(templatesDst, { recursive: true, force: true });
fs.mkdirSync(path.dirname(templatesDst), { recursive: true });
fs.cpSync(templatesSrc, templatesDst, { recursive: true, filter: (s) => !SKIP.test(s) });
console.log('synced templates β†’', path.relative(process.cwd(), templatesDst));
} else {
console.warn('starter template not found at', tplSrc, 'β€” desktop scaffold will fall back to the generic template');
console.warn('templates/ not found at', templatesSrc, 'β€” desktop scaffold will fall back to the generic template, and "New project" will be unavailable');
}

const binSrc = path.resolve(__dirname, '..', '..', 'bin', 'create-powercodex.js');
const binDst = path.resolve(__dirname, '..', 'vendor', 'bin', 'create-powercodex.js');
if (fs.existsSync(binSrc)) {
fs.mkdirSync(path.dirname(binDst), { recursive: true });
fs.copyFileSync(binSrc, binDst);
console.log('synced create-powercodex.js β†’', path.relative(process.cwd(), binDst));
} else {
console.warn('bin/create-powercodex.js not found β€” "New project" will be unavailable in this build');
}
1,049 changes: 1,049 additions & 0 deletions docs/superpowers/plans/2026-07-13-desktop-terminal-action-bridge.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Desktop terminal-action bridge

**Status:** approved (design), not yet planned/implemented
**Date:** 2026-07-13
**Author:** Manfred Siew (design session with Claude Code)

## Problem

The PowerCodex desktop app (`desktop/`) wraps the lifecycle chat UI (`tools/lifecycle/assets/chat.html`) in an Electron shell. The chat UI already bridges one terminal action end-to-end β€” "⚑ Build this" β€” through `Controller.action()` in `tools/lifecycle/lib/control.js`. Three more terminal actions a Code Apps maker regularly needs are **not** bridged:

- **Push** β€” `pac code push` exists as a working wrapper (`pushCodeApp()` in `tools/lifecycle/lib/pac-init.js`) and as a CLI subcommand (`code-push`), but `Controller.action()` has no case for it, so the chat UI can't call it.
- **Add datasource** β€” `pac code add-data-source` is only referenced as guidance text for the AI agent (`CODEAPPS['dataverse-specialist']` / `CODEAPPS['connector-integrator']` in `tools/lifecycle/lib/harness.js`). No wrapper function exists anywhere.
- **Create new project** β€” the full scaffold (starter template, OpenSpec, all 11 OPSX prompts/skills, npm install, git init) is `bin/create-powercodex.js`, a separate top-level CLI (`powercodex my-app`). The desktop app only has "πŸ“‚ Open project" (pick an existing folder) β€” nothing scaffolds a new one from inside the app.

The user should not need to leave the app or touch a terminal for these three actions. Clicking a button (or asking in chat) should be enough.

## Goal

Bridge all three actions into the chat UI using the same mechanism the app already uses for "Build this": a `Controller.action()` case that both a toolbar button and the chat agent call identically, streaming progress into the existing activity feed.

## Non-goals (explicitly skipped for v1)

- **No connector browser for non-Dataverse sources.** Free-text connector id only, until there's a real `pac connection list` wrapper to build a picker from.
- **No solution-aware push (`--solutionName`).** `alm-engineer` guidance mentions solutions as the unit of movement, but wiring Push to a specific solution is a distinct feature. v1 pushes to the default target the same way `pushCodeApp()` does today.
- **No rollback UI for Add-datasource.** Dataverse schema changes aren't cleanly reversible via the CLI anyway; the `allowPush` consent gate is the safety net, not an undo button.

## Design

### Core pattern

Every new action is one more `case` in `Controller.action()` (`tools/lifecycle/lib/control.js:44`) β€” the same switch that already handles `intake` / `approve` / `propose-mvp` / etc. This gives the button and the chat agent a single code path for free:

- The button calls `api('push', {...})` (the same `api()` helper `chat.html` already uses for every other action).
- The chat agent, when it recognizes intent in a typed message, calls the identical action.

No duplicated logic between "clicked" and "typed" β€” one implementation, two entry points.

### 1. Push (`case 'push'`)

- Wraps the existing `pushCodeApp()` from `tools/lifecycle/lib/pac-init.js`.
- Runs `npm run build` first if the workspace has a `build` script, then `pac code push`.
- Streams `pac` stdout/stderr into the activity feed via `emit()`, the same pattern `applySchema` already uses for Dataverse table creation.
- **Gate:** reuses the existing `allowPush` rights flag (the rights panel's "Publish to my environment" toggle). The button is disabled/tooltipped when `allowPush` is off; the chat path returns the same "Push is off β€” turn on Publish to my environment first" message the gate already produces elsewhere.
- **UI:** new "πŸš€ Push" toolbar button next to "⚑ Build this".
- **Chat intent:** recognizes phrases like "push", "deploy", "publish".

### 2. Add datasource (`case 'add-datasource'`)

- New module `tools/lifecycle/lib/datasource.js`, same spawn/emit shape as `pac-init.js`, wrapping `pac code add-data-source -a <api> -t <table>`.
- Unlike Push, this needs a parameter β€” no zero-input path:
- **Dataverse:** button opens a small panel listing tables already created via the Rule 1 browser flow, read from `.powercodex/dataverse.json` (`dataverse-schema.js`'s existing state file).
- **Other connectors:** free-text connector id field (see Non-goals β€” no picker yet).
- **Chat intent:** e.g. "add a datasource for the Orders table" β€” the agent extracts the table name and calls the same action. If it can't confidently identify a table/connector, it asks a clarifying question in chat rather than guessing.
- Guidance surfaced to the agent reuses the existing `CODEAPPS['dataverse-specialist']` / `CODEAPPS['connector-integrator']` text in `harness.js` β€” no new guidance text is authored.
- **Gate:** reuses `allowPush` (schema/data-source wiring is a live-environment change too).

### 3. Create new project (`case 'create-project'`)

- Spawns `node bin/create-powercodex.js <name>` as a child process into a folder the user picks via the existing native picker (`pcDesktop.pickFolder()`).
- Streams the CLI's real step names ("Copy starter template", "Initialize OpenSpec", "Finalize OPSX assets", etc. β€” from `create-powercodex.js`'s own `runStep()` calls) into the activity feed.
- On success, the app re-opens the new folder as the active workspace automatically (reusing the existing "open project" flow) β€” usable immediately, no extra step.
- **No gate** β€” purely local scaffolding, touches no live environment.
- **Chat intent:** e.g. "start a new project called Orders Tracker" β€” same action; if no name is given, asks for one (reusing `create-powercodex.js`'s existing `validateProjectName` error messages for consistency, rather than re-implementing validation).

## Cross-cutting notes

- All three actions follow the existing `emit()`-per-step convention, so the activity feed is the single "what happened" surface β€” no new progress UI to build.
- All three are exposed at the same `Controller.action()` switch, keeping button and chat-agent entry points identical.
- Skill guidance (the `CODEAPPS` object in `harness.js`) is already loaded into the agent's context for relevant tasks; these actions reuse that wiring rather than duplicating guidance text.
3 changes: 0 additions & 3 deletions package-lock.json

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

64 changes: 64 additions & 0 deletions templates/starter/tools/lifecycle/assets/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@
<button class="iconbtn" id="openVSCodeBtn" title="Open this project in VS Code">⧉ VS Code</button>
<button class="provchip" id="provChip" title="Which AI is answering β€” click to switch"><span class="br">🧠</span><span class="nm" id="provName">…</span><span style="color:var(--faint)">β–Ύ</span></button>
<button class="iconbtn" id="canvasToggle" title="Show the canvas">πŸ–ΌοΈ Canvas</button>
<button class="iconbtn" id="newProjectBtn" title="Create a brand-new PowerCodex project">✨ New project</button>
<button class="iconbtn" id="datasourceBtn" title="Wire up a Dataverse table or connector">🧩 Add datasource</button>
<button class="iconbtn" id="pushBtn" title="npm run build && pac code push" disabled>πŸš€ Push</button>
<button class="statusbtn" id="statusBtn"><span class="d"></span><span class="stg" id="stStage">Starting…</span><span class="cv" id="stHint">status β–Ύ</span></button>
</div>
</header>
Expand Down Expand Up @@ -377,6 +380,16 @@
</div>
</div>

<!-- add datasource -->
<div class="overlay" id="dsOverlay">
<div class="modal" id="dsPanel">
<div class="mh"><span>🧩</span><span class="t">Add a data source</span><button type="button" class="x" id="dsClose">βœ•</button></div>
<div class="fpbar"><select id="dsTableSelect"><option value="">β€” pick a Dataverse table β€”</option></select></div>
<div class="fpbar"><input id="dsConnectorInput" type="text" placeholder="…or a connector id, e.g. shared_sharepointonline" /></div>
<div class="mf"><span class="hint">Dataverse tables come from what's already been created via the browser flow. Other connectors need to exist in make.powerapps.com first.</span><button type="button" class="btn go" id="dsGo">Add data source</button></div>
</div>
</div>

<!-- AI sign-in gate (no silent degrade) -->
<div class="overlay" id="setupOverlay">
<div class="modal">
Expand Down Expand Up @@ -640,6 +653,9 @@
$('spCoverage').textContent = state.test && state.test.coverage!=null ? state.test.coverage+'%' : (state.intake && state.intake.compliance!=null ? state.intake.compliance+'%' : 'β€”');
$('spIssues').textContent = state.stopped ? 'needs you' : String((state.notifications||[]).filter(n=>n.kind==='approval').length);
$('spLast').textContent = (state.feed && state.feed[0] && state.feed[0].message) ? state.feed[0].message.slice(0,42) : 'β€”';
const rights = (state.intake && state.intake.rights) || {};
$('pushBtn').disabled = rights.allowPush !== true;
$('datasourceBtn').title = rights.allowPush === true ? 'Wire up a Dataverse table or connector' : 'Turn on "Publish to my environment" first';
}

// ---------- canvas ----------
Expand Down Expand Up @@ -995,6 +1011,54 @@
$('treeRefresh').onclick = ()=> loadTree();
makeSplit($('splitTree'),'left'); makeSplit($('splitCanvas'),'right');
$('openVSCodeBtn').onclick = openInVSCode;

// ---------- Push ----------
$('pushBtn').onclick = async () => {
$('pushBtn').disabled = true;
bubble('me', 'Push πŸš€');
const r = await api('push', {});
bubble('ai', r.ok ? 'βœ… Pushed to your environment.' : '⚠️ ' + esc(r.error || 'Push failed'));
if(app.treeRoot) loadTree();
};

// ---------- Add datasource ----------
async function openDatasourcePanel(){
$('dsOverlay').classList.add('open');
const sel = $('dsTableSelect');
sel.innerHTML = '<option value="">β€” pick a Dataverse table β€”</option>';
try {
const state = await (await fetch('/api/dataverse-state', {cache:'no-store'})).json();
(state.tables||[]).forEach(t => { if(t.logicalName){ const o=document.createElement('option'); o.value=t.logicalName; o.textContent=t.displayName+' ('+t.logicalName+')'; sel.appendChild(o); } });
} catch { /* picker still usable via the free-text connector field */ }
}
$('datasourceBtn').onclick = openDatasourcePanel;
$('dsClose').onclick = () => $('dsOverlay').classList.remove('open');
$('dsOverlay').onclick = (e) => { if(e.target===$('dsOverlay')) $('dsOverlay').classList.remove('open'); };
$('dsGo').onclick = async () => {
const table = $('dsTableSelect').value;
const connector = $('dsConnectorInput').value.trim();
$('dsOverlay').classList.remove('open');
bubble('me', table ? 'Add datasource: '+esc(table) : (connector ? 'Add datasource: '+esc(connector) : 'Add datasource'));
const r = await api('add-datasource', table ? {api:'dataverse', table} : {api: connector});
bubble('ai', r.ok ? 'βœ… Data source added.' : '⚠️ ' + esc(r.error || 'Could not add that data source'));
};

// ---------- Create new project ----------
$('newProjectBtn').onclick = async () => {
const name = prompt('Project name:');
if(!name) return;
let targetDir = null;
if(window.pcDesktop && window.pcDesktop.pickFolder) targetDir = await window.pcDesktop.pickFolder();
if(!targetDir) targetDir = prompt('Folder to create it in (absolute path):');
if(!targetDir) return;
bubble('me', 'New project: '+esc(name));
const typing = showTyping();
const r = await api('scaffold-project', {targetDir, name});
typing.remove();
bubble('ai', r.ok ? 'βœ… Created "'+esc(name)+'" and switched to it. It’s ready to build.' : '⚠️ ' + esc(r.error || 'Could not create the project'));
if(r.ok){ loadTree(); }
};

$('setupRecheck').onclick = recheckSetup;
$('setupSkip').onclick = ()=>{ app.skippedSetup=true; closeSetup(); };
$('setupOverlay').onclick = (e)=>{ if(e.target===$('setupOverlay')){ app.skippedSetup=true; closeSetup(); } };
Expand Down
Loading
Loading