Repository navigation
feat(extension): EULA gate — modal acceptance on first activation + packaged license asset #2139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| AMICODE — END USER LICENSE AGREEMENT | ||
| Version 0.1.0-placeholder (effective 2026-11-05) | ||
|
|
||
| *** PLACEHOLDER — NOT LEGAL TEXT *** | ||
| This file is a stand-in. Harmoniqs has not supplied the real End User License | ||
| Agreement yet. Everything below is placeholder material so the install-time | ||
| gate, the packaged license asset, and the review flow can be wired up and | ||
| tested. Replace this file with counsel-approved wording before any external | ||
| release. Do NOT ship this text as-is. | ||
| *** END PLACEHOLDER NOTICE *** | ||
|
|
||
| 1. Parties | ||
| This End User License Agreement ("Agreement") is a legal agreement between | ||
| you ("Licensee") and Harmoniqs, Inc. ("Harmoniqs", "we", "us") governing | ||
| your use of the Amicode extension for Visual Studio Code (the "Software"). | ||
|
|
||
| 2. Grant of License | ||
| Subject to your acceptance of this Agreement, Harmoniqs grants you a | ||
|
Comment on lines
+16
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| non-exclusive, non-transferable, revocable license to install and use the | ||
| Software for its intended purpose, in accordance with this Agreement. | ||
|
|
||
| 3. Restrictions (placeholder) | ||
| You may not: (a) reverse engineer the Software except as permitted by law; | ||
| (b) redistribute the Software except as permitted by its license notices; | ||
| (c) use the Software in violation of applicable law; (d) remove or alter | ||
| any proprietary notices in the Software. | ||
|
|
||
| 4. Beta / Preview Notice | ||
| The Software is currently a preview ("preview": true). It may contain | ||
| errors, change materially, or be withdrawn at any time. No SLA is offered. | ||
|
|
||
| 5. Data & Telemetry | ||
| The Software may include opt-in telemetry (see the amicode.telemetry.* | ||
|
Comment on lines
+29
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| settings). Telemetry is OFF by default and nothing is transmitted until you | ||
| explicitly consent in the product. See the README for details. | ||
|
|
||
| 6. No Warranty (placeholder) | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||
|
|
||
| 7. Limitation of Liability (placeholder) | ||
| IN NO EVENT SHALL HARMONIQS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, | ||
| SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING OUT OF OR IN CONNECTION | ||
| WITH THIS AGREEMENT OR THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE | ||
| POSSIBILITY OF SUCH DAMAGES. | ||
|
|
||
| 8. Termination | ||
| This Agreement terminates automatically upon breach of its terms. Upon | ||
| termination you must cease all use of the Software and uninstall it. | ||
|
|
||
| 9. Governing Law | ||
| This Agreement is governed by the laws of [PLACEHOLDER — jurisdiction to be | ||
| supplied with the final text], without regard to conflict-of-laws rules. | ||
|
|
||
| 10. Source Code License | ||
| The Amicode source code in this repository is licensed under the Apache | ||
| License, Version 2.0 (see the LICENSE file at the repository root). This | ||
| Agreement governs the packaged extension and its distribution to end | ||
| users; where the two conflict, this Agreement controls for the packaged | ||
| distribution. Third-party components retain their own licenses (see | ||
| THIRD_PARTY_LICENSES.md). | ||
|
|
||
| 11. Entire Agreement; Changes | ||
| This Agreement is the placeholder entire agreement between you and | ||
| Harmoniqs regarding the Software. Harmoniqs may update this Agreement; on | ||
| a material change the Software will require you to review and accept the | ||
| new version before it runs again. The version line at the top tracks | ||
| acceptance — bump it when the final text is in place. | ||
|
Comment on lines
+66
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| 12. Contact | ||
| Questions about this Agreement: [PLACEHOLDER — legal contact to be | ||
| supplied with the final text]. | ||
|
|
||
| Copyright (c) 2026 Harmoniqs, Inc. All rights reserved. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| // eula.ts — the install-time End User License Agreement gate. | ||
| // | ||
| // VS Code has no declarative, manifest-level "must accept the EULA to install" | ||
| // hook for extensions (verified against microsoft/vscode — the only shipping | ||
| // mechanism is the `license` manifest field, packaged by vsce as the | ||
| // `Microsoft.VisualStudio.Services.Content.License` gallery asset, shown under | ||
| // Resources on the Marketplace/Open VSX listing). The SUPPORTED way to make an | ||
| // end user actually agree is the pattern Microsoft uses for C/C++ (cpptools) and | ||
| // the IntelliJ extensions: a modal Accept/Decline prompt on first activation, | ||
| // persisted per machine, with the extension inert until acceptance. | ||
| // | ||
| // This module owns: | ||
| // - EULA_VERSION: bump it whenever the EULA text changes materially — recorded | ||
| // acceptance is keyed on it, so everyone is re-prompted on the new text. | ||
| // - isEulaAccepted(memento): the pure gate predicate (unit-tested). | ||
| // - eulaText(): the text shown when the user asks to read (EULA.txt from the | ||
| // extension root — the same file vsce packages as the License asset). | ||
| // - ensureEulaAccepted(): the activation-time gate. Runs the modal loop. | ||
| // - registerEulaCommand(): the `amicode.reviewEula` palette command, available | ||
| // in EVERY mode (full and gate) so a declining user can always come back. | ||
| // - registerEulaGateSurface(): the inert-mode UX — status bar item + a stub | ||
| // sidebar view (the contributed `amicode.workspace` view would otherwise | ||
| // render VS Code's generic "no provider registered" error pane). | ||
| // | ||
| // Acceptance lives in globalState (per machine), like the telemetry consent and | ||
| // the What's-New last-seen version. An install that accepted v<old> is NOT | ||
| // accepted for a bumped EULA_VERSION. | ||
| import * as vscode from "vscode"; | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
|
|
||
| export const EULA_ACCEPTED_KEY = "amicode.eula.acceptedVersion"; | ||
| /** The EULA text version acceptance is keyed on. Bump on any material change | ||
| * to EULA.txt (the final legal text lands as a bump, re-prompting all users). */ | ||
| export const EULA_VERSION = "0.1.0-placeholder"; | ||
| export const EULA_FILE = "EULA.txt"; | ||
|
|
||
| /** User-facing EULA version for UI chrome (sidebar footer, modal). The | ||
| * acceptance key stays EULA_VERSION ("0.1.0-placeholder") until the final | ||
| * legal text lands; the UI shows the clean version. */ | ||
| export const EULA_DISPLAY_VERSION = "0.1.0"; | ||
|
|
||
| const ACCEPT = "Accept EULA"; | ||
| const READ = "Read EULA"; | ||
| const DECLINE = "Decline"; | ||
|
|
||
| /** Embedded fallback so the read-path never dead-ends if EULA.txt is missing | ||
| * from the install (packaging regression). Deliberately short; the real text | ||
| * lives in EULA.txt. */ | ||
| export const PLACEHOLDER_EULA = `AMICODE — END USER LICENSE AGREEMENT | ||
| Version ${EULA_VERSION} (PLACEHOLDER) | ||
|
|
||
| The bundled EULA.txt could not be read from this installation. This is a | ||
| placeholder, not legal text — Harmoniqs has not supplied the final agreement. | ||
| By accepting you agree to be bound by the EULA shipped in this extension | ||
| (EULA.txt). Replace this file with counsel-approved wording before external | ||
| release. Copyright (c) 2026 Harmoniqs, Inc.`; | ||
|
|
||
| /** Minimal memento shape (matches the WhatsNewMemento convention). */ | ||
| export interface EulaMemento { | ||
| get<T>(key: string): T | undefined; | ||
| update(key: string, value: unknown): Thenable<void> | Promise<void>; | ||
| } | ||
|
|
||
| /** The gate predicate: accepted IFF the recorded version matches the current | ||
| * EULA_VERSION. Unset, an older version, or junk → not accepted. */ | ||
| export function isEulaAccepted(memento: EulaMemento | undefined): boolean { | ||
| if (!memento) return false; | ||
| return memento.get<unknown>(EULA_ACCEPTED_KEY) === EULA_VERSION; | ||
| } | ||
|
|
||
| /** The EULA text for the read-path: EULA.txt from the extension root, falling | ||
| * back to the embedded placeholder. Never throws. */ | ||
| export function eulaText(extensionPath: string, readFile: (p: string) => string = (p) => fs.readFileSync(p, "utf8")): string { | ||
| try { | ||
| const text = readFile(path.join(extensionPath, EULA_FILE)); | ||
| return text.trim() !== "" ? text : PLACEHOLDER_EULA; | ||
| } catch { | ||
| return PLACEHOLDER_EULA; | ||
| } | ||
| } | ||
|
|
||
| /** Open the EULA for reading: the real EULA.txt when present, else an untitled | ||
| * document carrying the embedded placeholder. */ | ||
| async function openEulaDocument(ctx: vscode.ExtensionContext): Promise<void> { | ||
| try { | ||
| const uri = vscode.Uri.file(path.join(ctx.extensionPath, EULA_FILE)); | ||
| const doc = await vscode.workspace.openTextDocument(uri); | ||
| await vscode.window.showTextDocument(doc, { preview: false }); | ||
| return; | ||
| } catch { | ||
| // fall through to the embedded placeholder | ||
| } | ||
| try { | ||
| const doc = await vscode.workspace.openTextDocument({ content: PLACEHOLDER_EULA, language: "plaintext" }); | ||
| await vscode.window.showTextDocument(doc, { preview: false }); | ||
| } catch { | ||
| /* no editor available — the modal keeps looping; the user can still accept or decline */ | ||
| } | ||
| } | ||
|
|
||
| const MODAL_DETAIL = | ||
| `Amicode is provided by Harmoniqs, Inc. under the End User License Agreement bundled with this extension ` + | ||
| `(EULA.txt, version ${EULA_DISPLAY_VERSION}).\n\n` + | ||
| `Accept to activate Amicode. Decline and Amicode stays inert — no server, no views, no commands — until you ` + | ||
| `accept (Command Palette → "Amicode: Review EULA").`; | ||
|
|
||
| /** The modal accept/read/decline loop. Resolves true iff the user accepted | ||
| * (and acceptance was recorded). Declining or dismissing the modal → false. */ | ||
| export async function promptForEula(ctx: vscode.ExtensionContext, log?: (line: string) => void): Promise<boolean> { | ||
| for (;;) { | ||
| const choice = await vscode.window.showInformationMessage( | ||
| "Amicode End User License Agreement", | ||
| { modal: true, detail: MODAL_DETAIL }, | ||
| ACCEPT, | ||
| READ, | ||
| DECLINE, | ||
| ); | ||
| if (choice === ACCEPT) { | ||
| await ctx.globalState.update(EULA_ACCEPTED_KEY, EULA_VERSION); | ||
| log?.(`[eula] EULA ${EULA_VERSION} accepted`); | ||
| return true; | ||
| } | ||
| if (choice === READ) { | ||
| await openEulaDocument(ctx); | ||
| continue; | ||
| } | ||
| log?.(`[eula] EULA ${EULA_VERSION} not accepted (choice: ${choice ?? "dismissed"})`); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** Activation-time gate. true = accepted (now or previously) → proceed with a | ||
| * full activation. false = declined/dismissed → caller registers the inert | ||
| * gate surface and returns. */ | ||
| export async function ensureEulaAccepted( | ||
| ctx: vscode.ExtensionContext, | ||
| log?: (line: string) => void, | ||
| ): Promise<boolean> { | ||
| if (isEulaAccepted(ctx.globalState)) return true; | ||
| return promptForEula(ctx, log); | ||
| } | ||
|
|
||
| async function reviewEula(ctx: vscode.ExtensionContext, log?: (line: string) => void): Promise<void> { | ||
| if (isEulaAccepted(ctx.globalState)) { | ||
| await openEulaDocument(ctx); | ||
| return; | ||
| } | ||
| const accepted = await promptForEula(ctx, log); | ||
| if (!accepted) return; | ||
| const reload = await vscode.window.showInformationMessage( | ||
| "Thank you — the Amicode EULA is accepted. Reload the window to finish activating Amicode?", | ||
| "Reload Window", | ||
| "Later", | ||
| ); | ||
| if (reload === "Reload Window") { | ||
| await vscode.commands.executeCommand("workbench.action.reloadWindow"); | ||
| } | ||
|
Comment on lines
+144
to
+158
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| /** Register `amicode.reviewEula`. Called in BOTH modes — in the full mode it | ||
| * re-opens the EULA for review; in the gate mode it is the only way back in. */ | ||
| export function registerEulaCommand(ctx: vscode.ExtensionContext, log?: (line: string) => void): void { | ||
| ctx.subscriptions.push(vscode.commands.registerCommand("amicode.reviewEula", () => void reviewEula(ctx, log))); | ||
| } | ||
|
Comment on lines
+161
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /** The inert surface for a declined/unaccepted install: the review command, a | ||
| * persistent status bar item, and a stub for the contributed `amicode.workspace` | ||
| * webview view so it renders a notice instead of VS Code's generic | ||
| * "no provider registered" error. */ | ||
| export function registerEulaGateSurface(ctx: vscode.ExtensionContext, log?: (line: string) => void): void { | ||
| registerEulaCommand(ctx, log); | ||
|
|
||
| const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); | ||
| status.text = "$(law) Amicode EULA"; | ||
| status.tooltip = "Amicode is disabled until you accept the End User License Agreement. Click to review."; | ||
| status.command = "amicode.reviewEula"; | ||
| status.show(); | ||
| ctx.subscriptions.push(status); | ||
|
|
||
| ctx.subscriptions.push( | ||
| vscode.window.registerWebviewViewProvider( | ||
| "amicode.workspace", | ||
| { | ||
| resolveWebviewView(webviewView) { | ||
| webviewView.webview.options = { | ||
| enableCommandUris: true, | ||
| // retain-exempt: static EULA notice — no SSE stream or session state to suspend when hidden | ||
| }; | ||
| webviewView.webview.html = eulaGateHtml(); | ||
| }, | ||
| }, | ||
| { webviewOptions: { retainContextWhenHidden: true } }, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| /** The inert sidebar body. Command URI → amicode.reviewEula (requires the | ||
| * webview's enableCommandUris, set above). */ | ||
| export function eulaGateHtml(): string { | ||
| return `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Amicode — EULA required</title> | ||
| <style> | ||
| body { font-family: var(--vscode-font-family, sans-serif); color: var(--vscode-foreground, #cccccc); | ||
| background: var(--vscode-sideBar-background, transparent); padding: 1.25em 1.1em; line-height: 1.5; } | ||
| h1 { font-size: 1.05em; margin: 0 0 .5em; font-weight: 600; } | ||
| p { margin: .4em 0; } | ||
| ul { margin: .6em 0 .8em; padding-left: 1.2em; } | ||
| li { margin: .2em 0; } | ||
| a.button { display: inline-block; margin: .6em 0 .8em; padding: .55em 1.15em; border-radius: 4px; | ||
| background: var(--vscode-button-background, #0e639c); color: var(--vscode-button-foreground, #ffffff); | ||
| text-decoration: none; font-weight: 500; } | ||
| a.button:hover { background: var(--vscode-button-hoverBackground, #1177bb); } | ||
| footer { margin-top: 1em; padding-top: .6em; border-top: 1px solid var(--vscode-panel-border, #2b2b2b); | ||
| font-size: .8em; opacity: .75; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Accept the EULA to activate Amicode</h1> | ||
| <p>Amicode is inert until you read and accept the End User License Agreement bundled with the extension | ||
| (<code>EULA.txt</code>, version ${EULA_DISPLAY_VERSION}).</p> | ||
| <ul> | ||
| <li>No server is spawned</li> | ||
| <li>No chat, views, or commands are active</li> | ||
| </ul> | ||
| <p>Accepting takes one click and is remembered on this machine.</p> | ||
| <a class="button" href="command:amicode.reviewEula">Review & Accept EULA…</a> | ||
| <footer>Amicode 0.4.0 · EULA ${EULA_DISPLAY_VERSION}</footer> | ||
| </body> | ||
| </html>`; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: harmoniqs/amicode
Length of output: 35346
Script executed:
Repository: harmoniqs/amicode
Length of output: 23037
Web query:
VS Code vsce 3.2 package license "SEE LICENSE IN" custom license file Marketplace license asset official documentationResult:
Script executed:
Repository: harmoniqs/amicode
Length of output: 19557
Replace the placeholder EULA before publishing.
If this version is released, users will be asked to accept unfinished terms, and the Marketplace license asset will contain the placeholder. Replace it with counsel-approved wording and bump
EULA_VERSIONbefore release.