workstation.md 0.1.1 → 0.3.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.
@@ -1,14 +1,2 @@
1
1
  #!/usr/bin/env node
2
- declare const API_URL: string;
3
- interface WorkstationInfo {
4
- id: string;
5
- host: string;
6
- port: number;
7
- web: string;
8
- }
9
- declare function apiRequest(path: string, options?: RequestInit): Promise<any>;
10
- declare function create(args: string[]): Promise<void>;
11
- declare function destroy(wsId: string): Promise<void>;
12
- declare function list(): Promise<void>;
13
- declare function usage(): void;
14
- declare function main(): Promise<void>;
2
+ export {};
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
2
+ import { execSync } from "child_process";
3
+ import { writeFileSync, mkdtempSync, readFileSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
3
6
  const API_URL = process.env.WORKSTATION_API_URL || "https://api.workstation.md";
4
7
  async function apiRequest(path, options = {}) {
5
8
  const res = await fetch(`${API_URL}${path}`, {
@@ -16,21 +19,65 @@ async function apiRequest(path, options = {}) {
16
19
  }
17
20
  return res.json();
18
21
  }
22
+ function findKeyPath(args) {
23
+ for (let i = 0; i < args.length; i++) {
24
+ if (args[i] === "--key" && args[i + 1]) {
25
+ return args[i + 1];
26
+ }
27
+ }
28
+ // Default: try common key paths
29
+ const home = process.env.HOME || "~";
30
+ const defaults = [
31
+ join(home, ".ssh", "id_ed25519"),
32
+ join(home, ".ssh", "id_rsa"),
33
+ ];
34
+ for (const p of defaults) {
35
+ try {
36
+ readFileSync(p);
37
+ return p;
38
+ }
39
+ catch { }
40
+ }
41
+ console.error("No SSH private key found. Use --key <path> to specify.");
42
+ process.exit(1);
43
+ }
44
+ function signWithSSHKey(keyPath, data) {
45
+ const tmp = mkdtempSync(join(tmpdir(), "ws-"));
46
+ const dataPath = join(tmp, "data");
47
+ writeFileSync(dataPath, data);
48
+ try {
49
+ execSync(`ssh-keygen -Y sign -f "${keyPath}" -n workstation.md "${dataPath}"`, { stdio: ["pipe", "pipe", "pipe"] });
50
+ const sigPath = dataPath + ".sig";
51
+ return readFileSync(sigPath, "utf-8");
52
+ }
53
+ catch (e) {
54
+ console.error("Failed to sign with SSH key:", e.stderr?.toString() || e.message);
55
+ process.exit(1);
56
+ }
57
+ }
19
58
  async function create(args) {
20
59
  let pubkey;
60
+ let name;
21
61
  for (let i = 0; i < args.length; i++) {
22
62
  if (args[i] === "--pubkey" && args[i + 1]) {
23
63
  pubkey = args[i + 1];
24
64
  i++;
25
65
  }
66
+ else if (args[i] === "--name" && args[i + 1]) {
67
+ name = args[i + 1];
68
+ i++;
69
+ }
26
70
  }
27
71
  if (!pubkey) {
28
- console.error("Usage: workstation create --pubkey <public_key>");
72
+ console.error("Usage: workstation create --pubkey <public_key> [--name <name>]");
29
73
  process.exit(1);
30
74
  }
75
+ const body = { pubkey };
76
+ if (name)
77
+ body.name = name;
31
78
  const info = await apiRequest("/create", {
32
79
  method: "POST",
33
- body: JSON.stringify({ pubkey }),
80
+ body: JSON.stringify(body),
34
81
  });
35
82
  console.log(JSON.stringify(info, null, 2));
36
83
  }
@@ -38,6 +85,15 @@ async function destroy(wsId) {
38
85
  const result = await apiRequest(`/${wsId}`, { method: "DELETE" });
39
86
  console.log(JSON.stringify(result, null, 2));
40
87
  }
88
+ async function extend(wsId, args) {
89
+ const keyPath = findKeyPath(args);
90
+ const signature = signWithSSHKey(keyPath, wsId);
91
+ const result = await apiRequest(`/${wsId}/extend`, {
92
+ method: "POST",
93
+ body: JSON.stringify({ signature }),
94
+ });
95
+ console.log(JSON.stringify(result, null, 2));
96
+ }
41
97
  async function list() {
42
98
  const items = await apiRequest("/list");
43
99
  if (items.length === 0) {
@@ -45,13 +101,17 @@ async function list() {
45
101
  return;
46
102
  }
47
103
  for (const ws of items) {
48
- console.log(`${ws.id}\tssh -p ${ws.port} root@${ws.host}\t${ws.web}`);
104
+ const ttl = ws.expires - Math.floor(Date.now() / 1000);
105
+ const hours = Math.max(0, Math.floor(ttl / 3600));
106
+ const mins = Math.max(0, Math.floor((ttl % 3600) / 60));
107
+ console.log(`${ws.id}\tssh -p ${ws.port} root@${ws.host}\t${ws.web}\t${hours}h${mins}m remaining`);
49
108
  }
50
109
  }
51
110
  function usage() {
52
111
  console.log(`Usage:
53
- workstation create --pubkey <public_key> Create a new workstation
112
+ workstation create --pubkey <key> [--name <name>] Create a workstation (24h TTL)
54
113
  workstation <id> destroy Destroy a workstation
114
+ workstation <id> extend [--key <path>] Extend TTL by 24h (proves key ownership)
55
115
  workstation list List active workstations`);
56
116
  }
57
117
  async function main() {
@@ -70,6 +130,9 @@ async function main() {
70
130
  else if (args.length >= 2 && args[1] === "destroy") {
71
131
  await destroy(command);
72
132
  }
133
+ else if (args.length >= 2 && args[1] === "extend") {
134
+ await extend(command, args.slice(2));
135
+ }
73
136
  else {
74
137
  usage();
75
138
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workstation.md",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Cloud Linux workstations for AI agents",
5
5
  "type": "module",
6
6
  "bin": {