@oxprotocol/cli 0.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.
Files changed (104) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +73 -0
  3. package/dist/cli.d.ts +15 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +109 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/create.d.ts +6 -0
  8. package/dist/commands/create.d.ts.map +1 -0
  9. package/dist/commands/create.js +212 -0
  10. package/dist/commands/create.js.map +1 -0
  11. package/dist/commands/dev.d.ts +2 -0
  12. package/dist/commands/dev.d.ts.map +1 -0
  13. package/dist/commands/dev.js +191 -0
  14. package/dist/commands/dev.js.map +1 -0
  15. package/dist/commands/doctor.d.ts +16 -0
  16. package/dist/commands/doctor.d.ts.map +1 -0
  17. package/dist/commands/doctor.js +197 -0
  18. package/dist/commands/doctor.js.map +1 -0
  19. package/dist/commands/install-url.d.ts +20 -0
  20. package/dist/commands/install-url.d.ts.map +1 -0
  21. package/dist/commands/install-url.js +236 -0
  22. package/dist/commands/install-url.js.map +1 -0
  23. package/dist/commands/install.d.ts +13 -0
  24. package/dist/commands/install.d.ts.map +1 -0
  25. package/dist/commands/install.js +411 -0
  26. package/dist/commands/install.js.map +1 -0
  27. package/dist/commands/keygen.d.ts +13 -0
  28. package/dist/commands/keygen.d.ts.map +1 -0
  29. package/dist/commands/keygen.js +40 -0
  30. package/dist/commands/keygen.js.map +1 -0
  31. package/dist/commands/login.d.ts +21 -0
  32. package/dist/commands/login.d.ts.map +1 -0
  33. package/dist/commands/login.js +224 -0
  34. package/dist/commands/login.js.map +1 -0
  35. package/dist/commands/logout.d.ts +17 -0
  36. package/dist/commands/logout.d.ts.map +1 -0
  37. package/dist/commands/logout.js +70 -0
  38. package/dist/commands/logout.js.map +1 -0
  39. package/dist/commands/pack.d.ts +12 -0
  40. package/dist/commands/pack.d.ts.map +1 -0
  41. package/dist/commands/pack.js +94 -0
  42. package/dist/commands/pack.js.map +1 -0
  43. package/dist/commands/protocol-register.d.ts +29 -0
  44. package/dist/commands/protocol-register.d.ts.map +1 -0
  45. package/dist/commands/protocol-register.js +231 -0
  46. package/dist/commands/protocol-register.js.map +1 -0
  47. package/dist/commands/publish.d.ts +17 -0
  48. package/dist/commands/publish.d.ts.map +1 -0
  49. package/dist/commands/publish.js +188 -0
  50. package/dist/commands/publish.js.map +1 -0
  51. package/dist/commands/token.d.ts +17 -0
  52. package/dist/commands/token.d.ts.map +1 -0
  53. package/dist/commands/token.js +110 -0
  54. package/dist/commands/token.js.map +1 -0
  55. package/dist/commands/whoami.d.ts +13 -0
  56. package/dist/commands/whoami.d.ts.map +1 -0
  57. package/dist/commands/whoami.js +77 -0
  58. package/dist/commands/whoami.js.map +1 -0
  59. package/dist/index.d.ts +26 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +22 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/lib/broadcast.d.ts +41 -0
  64. package/dist/lib/broadcast.d.ts.map +1 -0
  65. package/dist/lib/broadcast.js +45 -0
  66. package/dist/lib/broadcast.js.map +1 -0
  67. package/dist/lib/host-adapter.d.ts +72 -0
  68. package/dist/lib/host-adapter.d.ts.map +1 -0
  69. package/dist/lib/host-adapter.js +193 -0
  70. package/dist/lib/host-adapter.js.map +1 -0
  71. package/dist/lib/host-detect.d.ts +60 -0
  72. package/dist/lib/host-detect.d.ts.map +1 -0
  73. package/dist/lib/host-detect.js +479 -0
  74. package/dist/lib/host-detect.js.map +1 -0
  75. package/dist/lib/oxp-url.d.ts +34 -0
  76. package/dist/lib/oxp-url.d.ts.map +1 -0
  77. package/dist/lib/oxp-url.js +74 -0
  78. package/dist/lib/oxp-url.js.map +1 -0
  79. package/dist/lib/vsx-install.d.ts +41 -0
  80. package/dist/lib/vsx-install.d.ts.map +1 -0
  81. package/dist/lib/vsx-install.js +74 -0
  82. package/dist/lib/vsx-install.js.map +1 -0
  83. package/dist/util.d.ts +10 -0
  84. package/dist/util.d.ts.map +1 -0
  85. package/dist/util.js +64 -0
  86. package/dist/util.js.map +1 -0
  87. package/package.json +48 -0
  88. package/templates/hello-code/README.md +39 -0
  89. package/templates/hello-code/oxp.json +14 -0
  90. package/templates/hello-code/package.json +18 -0
  91. package/templates/hello-code/src/extension.ts +17 -0
  92. package/templates/hello-code/tsconfig.json +20 -0
  93. package/templates/hello-html/README.md +18 -0
  94. package/templates/hello-html/oxp.json +14 -0
  95. package/templates/hello-html/ui/index.html +43 -0
  96. package/templates/hello-rust/Cargo.toml +19 -0
  97. package/templates/hello-rust/README.md +50 -0
  98. package/templates/hello-rust/oxp.json +22 -0
  99. package/templates/hello-rust/src/lib.rs +111 -0
  100. package/templates/hello-rust/wit/deps/oxp-host/oxp-host.wit +136 -0
  101. package/templates/hello-rust/wit/extension.wit +71 -0
  102. package/templates/hello-tree/README.md +15 -0
  103. package/templates/hello-tree/oxp.json +14 -0
  104. package/templates/hello-tree/ui/tree.json +27 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * VSX install adapter — handles `oxp install @publisher/slug` for entries
3
+ * that are mirrored from Open VSX rather than published natively to OXP.
4
+ *
5
+ * Strategy:
6
+ * 1. Caller pre-fetched `/api/v1/extensions/<publisher>/<slug>` and
7
+ * saw a `vsx` block in the response → call `installVsx()`.
8
+ * 2. We detect the user's VS Code-family IDEs (already done by the
9
+ * install command) and run `<cli> --install-extension <ns>.<name>`
10
+ * against each one. The IDE's own install path handles VSIX fetch,
11
+ * signature checks (or lack thereof), and registration.
12
+ * 3. We DO NOT touch the OXP shared host-store — VSX extensions are
13
+ * not OXP wasm components and have no oxp.json runtime contract.
14
+ *
15
+ * This keeps the CLI honest: VSX entries are explicitly delegated to
16
+ * the IDE, and we surface that in CLI output so the user knows what
17
+ * happened.
18
+ */
19
+ import { spawnSync } from "node:child_process";
20
+ /**
21
+ * Run `<cliPath> --install-extension <namespace>.<name>` against every
22
+ * detected vscode-family host. JetBrains/Zed/Piye are reported as
23
+ * skipped since they have no compatible install command for VSIX.
24
+ */
25
+ export function installVsx(target, hosts) {
26
+ const ext = `${target.namespace}.${target.name}`;
27
+ const out = [];
28
+ for (const host of hosts) {
29
+ if (host.family !== "vscode") {
30
+ out.push({
31
+ host,
32
+ status: "skipped",
33
+ reason: `family ${host.family} can't install VSIX via --install-extension`,
34
+ });
35
+ continue;
36
+ }
37
+ if (!host.cliPath) {
38
+ out.push({
39
+ host,
40
+ status: "skipped",
41
+ reason: "no CLI launcher on PATH",
42
+ });
43
+ continue;
44
+ }
45
+ const res = spawnSync(host.cliPath, ["--install-extension", ext], {
46
+ encoding: "utf8",
47
+ // Cap runtime so a hung IDE process can't block the CLI forever.
48
+ timeout: 60_000,
49
+ });
50
+ const tail = (res.stdout || "") + (res.stderr || "");
51
+ const trimmed = tail.length > 400 ? tail.slice(-400) : tail;
52
+ if (res.error) {
53
+ out.push({
54
+ host,
55
+ status: "failed",
56
+ reason: res.error.message,
57
+ output: trimmed,
58
+ });
59
+ continue;
60
+ }
61
+ if (res.status !== 0) {
62
+ out.push({
63
+ host,
64
+ status: "failed",
65
+ reason: `${host.cliPath} exited with code ${res.status}`,
66
+ output: trimmed,
67
+ });
68
+ continue;
69
+ }
70
+ out.push({ host, status: "ok", output: trimmed });
71
+ }
72
+ return out;
73
+ }
74
+ //# sourceMappingURL=vsx-install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vsx-install.js","sourceRoot":"","sources":["../../src/lib/vsx-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAqB/C;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,MAAwB,EACxB,KAAqB;IAErB,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;IACjD,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI;gBACJ,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,UAAU,IAAI,CAAC,MAAM,6CAA6C;aAC3E,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI;gBACJ,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,yBAAyB;aAClC,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,qBAAqB,EAAE,GAAG,CAAC,EAAE;YAChE,QAAQ,EAAE,MAAM;YAChB,iEAAiE;YACjE,OAAO,EAAE,MAAM;SAChB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5D,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI;gBACJ,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO;gBACzB,MAAM,EAAE,OAAO;aAChB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI;gBACJ,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,qBAAqB,GAAG,CAAC,MAAM,EAAE;gBACxD,MAAM,EAAE,OAAO;aAChB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** Shared helpers for OXP CLI commands. */
2
+ export declare function oxpHome(): string;
3
+ export declare function registryUrl(): string;
4
+ export declare function readCredentials(): Promise<string | null>;
5
+ export declare function writeCredentials(token: string): Promise<void>;
6
+ /** Find the project root (first ancestor containing oxp.json). */
7
+ export declare function findProjectRoot(start: string): Promise<string | null>;
8
+ export declare function info(msg: string): void;
9
+ export declare function fail(msg: string): never;
10
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAM3C,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAKD,wBAAgB,WAAW,IAAI,MAAM,CAgBpC;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO9D;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnE;AAED,kEAAkE;AAClE,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAa3E;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtC;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAGvC"}
package/dist/util.js ADDED
@@ -0,0 +1,64 @@
1
+ /** Shared helpers for OXP CLI commands. */
2
+ import { promises as fs } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ export function oxpHome() {
6
+ return process.env.OXP_HOME ?? join(homedir(), ".oxp");
7
+ }
8
+ const DEFAULT_REGISTRY = "http://localhost:3000";
9
+ let warnedDefaultRegistry = false;
10
+ export function registryUrl() {
11
+ const fromEnv = process.env.OXP_REGISTRY;
12
+ if (fromEnv && fromEnv.trim().length > 0) {
13
+ return fromEnv.replace(/\/+$/, "");
14
+ }
15
+ // Localhost default is convenient for self-host / dev, but if you're trying
16
+ // to publish or install against the public registry it's almost always a
17
+ // mistake. Warn once per process so it's visible without becoming spammy.
18
+ if (!warnedDefaultRegistry) {
19
+ warnedDefaultRegistry = true;
20
+ process.stderr.write(`[oxp] OXP_REGISTRY not set — defaulting to ${DEFAULT_REGISTRY}. ` +
21
+ `Set OXP_REGISTRY=https://… to target a different host.\n`);
22
+ }
23
+ return DEFAULT_REGISTRY;
24
+ }
25
+ export async function readCredentials() {
26
+ try {
27
+ const buf = await fs.readFile(join(oxpHome(), "credentials"), "utf8");
28
+ return buf.trim() || null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export async function writeCredentials(token) {
35
+ const dir = oxpHome();
36
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
37
+ const path = join(dir, "credentials");
38
+ await fs.writeFile(path, token + "\n", { mode: 0o600 });
39
+ }
40
+ /** Find the project root (first ancestor containing oxp.json). */
41
+ export async function findProjectRoot(start) {
42
+ let cur = resolve(start);
43
+ while (true) {
44
+ try {
45
+ await fs.access(join(cur, "oxp.json"));
46
+ return cur;
47
+ }
48
+ catch {
49
+ // continue
50
+ }
51
+ const parent = dirname(cur);
52
+ if (parent === cur)
53
+ return null;
54
+ cur = parent;
55
+ }
56
+ }
57
+ export function info(msg) {
58
+ process.stdout.write(msg + "\n");
59
+ }
60
+ export function fail(msg) {
61
+ process.stderr.write(`oxp: ${msg}\n`);
62
+ process.exit(1);
63
+ }
64
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@oxprotocol/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line tool for the Open eXtensions Protocol.",
5
+ "type": "module",
6
+ "bin": {
7
+ "oxp": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./create": {
15
+ "types": "./dist/commands/create.d.ts",
16
+ "default": "./dist/commands/create.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "templates",
22
+ "README.md"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "chokidar": "^4.0.3",
29
+ "ws": "^8.18.0",
30
+ "@oxprotocol/bundle": "0.1.0",
31
+ "@oxprotocol/host-core": "0.1.0",
32
+ "@oxprotocol/wit": "0.1.0",
33
+ "@oxprotocol/types": "0.1.0",
34
+ "@oxprotocol/schema": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.10.2",
38
+ "@types/ws": "^8.5.13",
39
+ "tsx": "^4.20.6",
40
+ "typescript": "5.9.3",
41
+ "vitest": "^2.1.8"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
45
+ "dev": "tsx src/cli.ts",
46
+ "test": "vitest run --passWithNoTests"
47
+ }
48
+ }
@@ -0,0 +1,39 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ OXP code extension scaffolded from the `hello-code` template.
4
+
5
+ ## Develop
6
+
7
+ ```sh
8
+ pnpm install
9
+ pnpm dev # start the dev loop (watch + hot-reload)
10
+ ```
11
+
12
+ ## Publish
13
+
14
+ ```sh
15
+ pnpm pack # build → dist/__SLUG__-0.0.1.oxp
16
+ oxp publish # upload to the registry
17
+ ```
18
+
19
+ ## Capabilities
20
+
21
+ Declare what your extension needs in `oxp.json` → `permissions`:
22
+
23
+ - `read-clipboard`
24
+ - `write-clipboard`
25
+ - `storage:local`
26
+ - `network:<domain>` (e.g. `network:api.example.com`, or `network:*`)
27
+
28
+ Use them via the `host` API:
29
+
30
+ ```ts
31
+ import { defineExtension, clipboard, net } from "@oxprotocol/sdk";
32
+
33
+ export default defineExtension({
34
+ async activate(host) {
35
+ const cb = clipboard(host);
36
+ await cb.write("hello");
37
+ },
38
+ });
39
+ ```
@@ -0,0 +1,14 @@
1
+ {
2
+ "specVersion": "1",
3
+ "id": "@__PUBLISHER__/__SLUG__",
4
+ "publisher": "__PUBLISHER__",
5
+ "version": "0.0.1",
6
+ "displayName": "__DISPLAY_NAME__",
7
+ "description": "An OXP code extension built with @oxprotocol/sdk.",
8
+ "license": "MIT",
9
+ "categories": ["other"],
10
+ "engines": { "oxp": "^1.0.0" },
11
+ "main": { "entry": "dist/extension.js" },
12
+ "ui": { "preferredSurface": "panel" },
13
+ "permissions": []
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@__PUBLISHER__/__SLUG__",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json",
8
+ "dev": "oxp dev",
9
+ "pack": "pnpm build && oxp pack",
10
+ "publish:oxp": "pnpm pack && oxp publish"
11
+ },
12
+ "dependencies": {
13
+ "@oxprotocol/sdk": "^0.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.6.0"
17
+ }
18
+ }
@@ -0,0 +1,17 @@
1
+ import { defineExtension } from "@oxprotocol/sdk";
2
+
3
+ export default defineExtension({
4
+ async activate(host) {
5
+ host.log("info", "hello from @__PUBLISHER__/__SLUG__");
6
+ host.renderHtml(`
7
+ <div style="font-family:system-ui;padding:2rem;color:#f8fafc;background:#060a13;min-height:100vh;display:grid;place-items:center">
8
+ <div style="border:1px solid rgba(125,211,252,.3);padding:2rem;border-radius:8px;text-align:center">
9
+ <h1 style="margin:0 0 .5rem;font-size:1.5rem">__DISPLAY_NAME__</h1>
10
+ <p style="margin:0;opacity:.6;font-family:ui-monospace,monospace;font-size:.75rem">
11
+ @__PUBLISHER__/__SLUG__
12
+ </p>
13
+ </div>
14
+ </div>
15
+ `);
16
+ },
17
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM",
9
+ "WebWorker"
10
+ ],
11
+ "strict": true,
12
+ "outDir": "dist",
13
+ "rootDir": "src",
14
+ "declaration": false,
15
+ "skipLibCheck": true
16
+ },
17
+ "include": [
18
+ "src"
19
+ ]
20
+ }
@@ -0,0 +1,18 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ A minimal OXP extension scaffolded by `oxp create`.
4
+
5
+ ## Build
6
+
7
+ ```sh
8
+ oxp pack
9
+ ```
10
+
11
+ Writes `dist/__SLUG__-<version>.oxp` and `dist/__SLUG__-<version>.sig.json`.
12
+
13
+ ## Publish
14
+
15
+ ```sh
16
+ oxp login # paste an API token from the registry
17
+ oxp publish dist/__SLUG__-*.oxp
18
+ ```
@@ -0,0 +1,14 @@
1
+ {
2
+ "specVersion": "1",
3
+ "id": "@__PUBLISHER__/__SLUG__",
4
+ "publisher": "__PUBLISHER__",
5
+ "version": "0.0.1",
6
+ "displayName": "__DISPLAY_NAME__",
7
+ "description": "Hello, OXP world.",
8
+ "license": "MIT",
9
+ "categories": ["other"],
10
+ "engines": { "oxp": "^1.0.0" },
11
+ "main": { "ui": "ui/index.html" },
12
+ "ui": { "components": "oxp-ui-only", "preferredSurface": "panel" },
13
+ "permissions": []
14
+ }
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>__DISPLAY_NAME__</title>
6
+ <style>
7
+ :root {
8
+ color-scheme: dark light;
9
+ }
10
+ body {
11
+ margin: 0;
12
+ min-height: 100vh;
13
+ display: grid;
14
+ place-items: center;
15
+ font-family: -apple-system, system-ui, sans-serif;
16
+ background: #060a13;
17
+ color: #f8fafc;
18
+ }
19
+ .card {
20
+ border: 1px solid rgba(125, 211, 252, 0.3);
21
+ padding: 2rem 2.5rem;
22
+ border-radius: 8px;
23
+ text-align: center;
24
+ }
25
+ h1 {
26
+ margin: 0 0 0.5rem;
27
+ font-size: 1.75rem;
28
+ }
29
+ p {
30
+ margin: 0;
31
+ opacity: 0.6;
32
+ font-family: ui-monospace, monospace;
33
+ font-size: 0.75rem;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div class="card">
39
+ <h1>Hello, OXP world.</h1>
40
+ <p>@__PUBLISHER__/__SLUG__</p>
41
+ </div>
42
+ </body>
43
+ </html>
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "__SLUG_UNDERSCORED__"
3
+ version = "0.0.1"
4
+ edition = "2021"
5
+ publish = false
6
+
7
+ [lib]
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ wit-bindgen = "0.36"
12
+
13
+ [profile.release]
14
+ # These flags shrink the resulting .wasm dramatically. Hello-world
15
+ # components built with this profile land under 50 KB.
16
+ opt-level = "s"
17
+ lto = true
18
+ strip = true
19
+ codegen-units = 1
@@ -0,0 +1,50 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ A `component-v1` OXP extension written in Rust, targeting the
4
+ [WASI Preview 2](https://github.com/WebAssembly/WASI) component model
5
+ and the canonical `oxp:extension@0.1.0` world.
6
+
7
+ ## Prerequisites
8
+
9
+ ```sh
10
+ rustup target add wasm32-wasip2
11
+ ```
12
+
13
+ ## Build & pack
14
+
15
+ ```sh
16
+ oxp pack
17
+ # → runs `scripts.build` from oxp.json (cargo build + copy)
18
+ # → writes dist/__SLUG__-0.0.1.oxp
19
+ ```
20
+
21
+ If you want to skip the build hook (e.g. you've built manually):
22
+
23
+ ```sh
24
+ oxp pack --no-build
25
+ ```
26
+
27
+ ## What does it do?
28
+
29
+ The `activate` lifecycle export logs a single line via the always-on
30
+ `oxp:host/log` interface. `deactivate` logs `"goodbye"`. That's the
31
+ whole extension — it's deliberately minimal so you can verify the
32
+ toolchain end-to-end before adding logic.
33
+
34
+ ## Layout
35
+
36
+ ```
37
+ oxp.json OXP manifest (component-v1, pins oxp:extension@0.1.0)
38
+ Cargo.toml Rust crate definition (cdylib)
39
+ src/lib.rs Component implementation (lifecycle + ui-handler + command-handler)
40
+ wit/ WIT contract — extension.wit + deps/oxp-host/oxp-host.wit
41
+ build/ Built .wasm artefact — packed into the bundle
42
+ dist/ Output of `oxp pack` (.oxp + signature); gitignored
43
+ ```
44
+
45
+ The `wit/` directory is shipped inline so the project builds without
46
+ any monorepo dependency. If the upstream `oxp:extension` world ever
47
+ changes you'll need to refresh the files and update `oxp.json#wit.sha256`
48
+ to match — `oxp create --template hello-rust` always emits the current
49
+ hash, so the easy fix is to scaffold a fresh project and copy the new
50
+ WIT files + sha across.
@@ -0,0 +1,22 @@
1
+ {
2
+ "specVersion": "1",
3
+ "id": "@__PUBLISHER__/__SLUG__",
4
+ "publisher": "__PUBLISHER__",
5
+ "version": "0.0.1",
6
+ "displayName": "__DISPLAY_NAME__",
7
+ "description": "OXP component-v1 extension written in Rust.",
8
+ "license": "MIT",
9
+ "categories": ["other"],
10
+ "engines": { "oxp": "^1.0.0" },
11
+ "kind": "component-v1",
12
+ "main": { "wasm": "build/__SLUG_UNDERSCORED__.wasm" },
13
+ "scripts": {
14
+ "build": "cargo build --release --target wasm32-wasip2 && mkdir -p build && cp target/wasm32-wasip2/release/__SLUG_UNDERSCORED__.wasm build/"
15
+ },
16
+ "permissions": [],
17
+ "wit": {
18
+ "package": "oxp:extension",
19
+ "version": "0.1.0",
20
+ "sha256": "__WIT_SHA__"
21
+ }
22
+ }
@@ -0,0 +1,111 @@
1
+ // Hello-world OXP extension component, written in Rust.
2
+ //
3
+ // Exercises:
4
+ // * activate / deactivate lifecycle exports
5
+ // * a single always-on host import: oxp:host/log
6
+ //
7
+ // To build:
8
+ // rustup target add wasm32-wasip2
9
+ // cargo build --release --target wasm32-wasip2
10
+ // mkdir -p build && cp target/wasm32-wasip2/release/__SLUG_UNDERSCORED__.wasm build/
11
+ // oxp pack
12
+
13
+ wit_bindgen::generate!({
14
+ world: "extension",
15
+ path: "wit",
16
+ generate_all,
17
+ });
18
+
19
+ use exports::oxp::extension::command_handler::Guest as CommandHandlerGuest;
20
+ use exports::oxp::extension::lifecycle::{ActivateCtx, Guest as LifecycleGuest};
21
+ use exports::oxp::extension::ui_handler::{EventError, Guest as UiHandlerGuest};
22
+ use oxp::host::log::{log, Level};
23
+
24
+ struct Component;
25
+
26
+ impl LifecycleGuest for Component {
27
+ fn activate(ctx: ActivateCtx) -> Result<(), String> {
28
+ log(
29
+ Level::Info,
30
+ &format!(
31
+ "hello from {} v{} on host {} ({})",
32
+ ctx.extension_id, ctx.version, ctx.host, ctx.host_version
33
+ ),
34
+ );
35
+ Ok(())
36
+ }
37
+
38
+ fn deactivate() -> Result<(), String> {
39
+ log(Level::Info, "goodbye");
40
+ Ok(())
41
+ }
42
+ }
43
+
44
+ impl UiHandlerGuest for Component {
45
+ fn on_event(_event: Vec<u8>) -> Result<(), EventError> {
46
+ Ok(())
47
+ }
48
+ }
49
+
50
+ impl CommandHandlerGuest for Component {
51
+ fn on_command(id: String, args_json: String) -> Result<String, String> {
52
+ match id.as_str() {
53
+ // Replace this with your own command. The result must be valid JSON.
54
+ "hello.greet" => {
55
+ let name = parse_json_string_field(&args_json, "name")
56
+ .unwrap_or_else(|| "world".to_string());
57
+ Ok(format!("\"hello, {}!\"", json_escape(&name)))
58
+ }
59
+ other => Ok(format!("\"unhandled:{}\"", other)),
60
+ }
61
+ }
62
+ }
63
+
64
+ // Tiny zero-dependency JSON helpers so the template doesn't pull in serde
65
+ // just for one example. Replace with serde_json once your extension grows.
66
+ fn parse_json_string_field(json: &str, field: &str) -> Option<String> {
67
+ let needle = format!("\"{field}\"");
68
+ let start = json.find(&needle)? + needle.len();
69
+ let rest = &json[start..];
70
+ let colon = rest.find(':')? + 1;
71
+ let after_colon = &rest[colon..];
72
+ let q = after_colon.find('"')? + 1;
73
+ let body = &after_colon[q..];
74
+ let mut out = String::new();
75
+ let mut chars = body.chars();
76
+ while let Some(c) = chars.next() {
77
+ if c == '\\' {
78
+ match chars.next()? {
79
+ 'n' => out.push('\n'),
80
+ 't' => out.push('\t'),
81
+ 'r' => out.push('\r'),
82
+ '"' => out.push('"'),
83
+ '\\' => out.push('\\'),
84
+ other => out.push(other),
85
+ }
86
+ } else if c == '"' {
87
+ return Some(out);
88
+ } else {
89
+ out.push(c);
90
+ }
91
+ }
92
+ None
93
+ }
94
+
95
+ fn json_escape(s: &str) -> String {
96
+ let mut out = String::with_capacity(s.len());
97
+ for c in s.chars() {
98
+ match c {
99
+ '"' => out.push_str("\\\""),
100
+ '\\' => out.push_str("\\\\"),
101
+ '\n' => out.push_str("\\n"),
102
+ '\r' => out.push_str("\\r"),
103
+ '\t' => out.push_str("\\t"),
104
+ c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
105
+ c => out.push(c),
106
+ }
107
+ }
108
+ out
109
+ }
110
+
111
+ export!(Component);