@instafy/cli 0.1.0-staging.138
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.
- package/README.md +66 -0
- package/bin/instafy.js +5 -0
- package/dist/index.js +247 -0
- package/dist/org.js +46 -0
- package/dist/project.js +184 -0
- package/dist/rathole.js +212 -0
- package/dist/runtime.js +709 -0
- package/dist/tunnel.js +180 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Instafy CLI (developer preview)
|
|
2
|
+
|
|
3
|
+
Local helper for running the Instafy runtime (agent + origin) on desktop or self-hosted servers. Primary commands:
|
|
4
|
+
|
|
5
|
+
- `instafy runtime:start` — launch the runtime in the current machine (foreground by default, `--detach` to background).
|
|
6
|
+
- `instafy runtime:status` — report health of the last started runtime.
|
|
7
|
+
- `instafy runtime:stop` — stop the last started runtime.
|
|
8
|
+
- `instafy runtime:token` — mint a runtime access token with agent + origin scopes.
|
|
9
|
+
- `instafy project:init` — create a project via the controller and write `.instafy/project.json` in the chosen folder.
|
|
10
|
+
- `instafy project:list` — list projects for the orgs available to the current controller token.
|
|
11
|
+
- `instafy org:list` — list orgs available to the current controller token.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# From repo root (assumes runtime-agent is built): pnpm --filter @instafy/cli dev -- --help
|
|
17
|
+
pnpm --filter @instafy/cli dev -- runtime:start \
|
|
18
|
+
--project 00000000-0000-0000-0000-000000000000 \
|
|
19
|
+
--controller-url http://127.0.0.1:8788 \
|
|
20
|
+
--controller-access-token "$CONTROLLER_ACCESS_TOKEN"
|
|
21
|
+
|
|
22
|
+
# Inspect status
|
|
23
|
+
pnpm --filter @instafy/cli dev -- runtime:status
|
|
24
|
+
|
|
25
|
+
# Stop
|
|
26
|
+
pnpm --filter @instafy/cli dev -- runtime:stop
|
|
27
|
+
|
|
28
|
+
# Create a project manifest for the current directory
|
|
29
|
+
pnpm --filter @instafy/cli dev -- project:init --controller-url http://127.0.0.1:8788 --access-token "$SUPABASE_ACCESS_TOKEN"
|
|
30
|
+
|
|
31
|
+
# List projects for your orgs
|
|
32
|
+
pnpm --filter @instafy/cli dev -- project:list --controller-url http://127.0.0.1:8788 --access-token "$SUPABASE_ACCESS_TOKEN"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Auth model
|
|
36
|
+
|
|
37
|
+
- The CLI mints a **runtime access token** via `/projects/:projectId/runtime/token` when a controller access token is provided (`--controller-access-token` or `CONTROLLER_ACCESS_TOKEN`).
|
|
38
|
+
- That token is exported to the runtime as `RUNTIME_ACCESS_TOKEN` and mirrored to `ORIGIN_INTERNAL_TOKEN` for the bundled origin server.
|
|
39
|
+
- You can supply a pre-minted token with `--runtime-token` / `RUNTIME_ACCESS_TOKEN` (also reused for origin). Legacy `--origin-token` still maps to the same value.
|
|
40
|
+
- If you only have a Supabase session (service role or user JWT), pass it via `--supabase-access-token`, `--supabase-access-token-file`, or `SUPABASE_ACCESS_TOKEN`. The CLI uses that session to mint the controller and runtime tokens automatically so you do not need to juggle controller-issued keys locally.
|
|
41
|
+
|
|
42
|
+
## Important flags
|
|
43
|
+
|
|
44
|
+
- `--project` / `PROJECT_ID` — project UUID to associate the runtime with (defaults to `.instafy/project.json` if present).
|
|
45
|
+
- `--controller-url` / `CONTROLLER_BASE_URL` — controller base URL (defaults to `http://127.0.0.1:8788`).
|
|
46
|
+
- `--controller-access-token` — user/controller token used to mint the runtime token if `--runtime-token` is not provided.
|
|
47
|
+
- `--supabase-access-token` / `SUPABASE_ACCESS_TOKEN` — Supabase session token the CLI can exchange for controller and runtime tokens when you do not have a controller key handy.
|
|
48
|
+
- `--runtime-token` / `RUNTIME_ACCESS_TOKEN` — pre-minted runtime/origin token to bypass minting.
|
|
49
|
+
- `--origin-id` — optionally fix the origin id (otherwise auto-generated).
|
|
50
|
+
- `--origin-endpoint` — skip tunnels entirely and publish a fixed, reachable origin URL (useful for servers with a stable public endpoint).
|
|
51
|
+
- `--workspace` / `WORKSPACE_DIR` — workspace root for the runtime (default `./.instafy/workspace`).
|
|
52
|
+
- `--provider` / `RUNTIME_PROVIDER` — runtime provider label (defaults to the org in `.instafy/project.json`, otherwise `self-hosted`).
|
|
53
|
+
- `--detach` — background the runtime; pid/log state is recorded under `~/.instafy/cli-runtime-state.json`.
|
|
54
|
+
- `--org-id` — target an existing org when running `project:init` (otherwise the CLI creates or reuses a personal org).
|
|
55
|
+
|
|
56
|
+
## Tunnels (self-hosted)
|
|
57
|
+
|
|
58
|
+
Tunnels are controller-managed: the runtime-agent requests a tunnel assignment from the controller and then launches the appropriate client locally.
|
|
59
|
+
|
|
60
|
+
- **Self-hosted tunnels (recommended)**: controller is configured with `TUNNEL_BROKER_BASE_URL` / `TUNNEL_BROKER_TOKEN` and returns `provider=self_hosted`. The runtime-agent launches `rathole` (outbound-only). The CLI will try to resolve `RATHOLE_BIN` (PATH or cached download). On macOS arm64, it falls back to `cargo install rathole` when no prebuilt is available.
|
|
61
|
+
|
|
62
|
+
## Notes
|
|
63
|
+
|
|
64
|
+
- The CLI reuses the locally built `runtime-agent` binary from `target/{debug,release}`. Run `cargo build -p runtime-agent` first or set `INSTAFY_RUNTIME_AGENT_BIN` to an explicit path.
|
|
65
|
+
- Runtime/origin tokens require the controller to be configured with the Ed25519 signing pair (`RUNTIME_SIGNING_PRIVATE_KEY` / `RUNTIME_SIGNING_PUBLIC_KEY`; legacy `ORIGIN_TOKEN_*` is also accepted).
|
|
66
|
+
- Logs for detached runs are written to `~/.instafy/cli-runtime-logs/*`.
|
package/bin/instafy.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, mountWorkspace, } from "./runtime.js";
|
|
5
|
+
import { projectInit } from "./project.js";
|
|
6
|
+
import { runTunnelCommand } from "./tunnel.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("instafy")
|
|
10
|
+
.description("Instafy CLI — manage local/self-hosted runtimes")
|
|
11
|
+
.version("0.1.0");
|
|
12
|
+
program
|
|
13
|
+
.command("runtime:start")
|
|
14
|
+
.description("Start a local Instafy runtime (agent + origin)")
|
|
15
|
+
.option("--project <id>", "Project UUID")
|
|
16
|
+
.option("--controller-url <url>", "Controller base URL")
|
|
17
|
+
.option("--controller-token <token>", "Controller access token")
|
|
18
|
+
.option("--controller-access-token <token>", "Controller-issued user token used to mint runtime/origin credentials")
|
|
19
|
+
.option("--supabase-access-token <token>", "Supabase session token used to mint controller/runtime tokens")
|
|
20
|
+
.option("--supabase-access-token-file <path>", "File containing the Supabase session token")
|
|
21
|
+
.option("--runtime-token <token>", "Pre-minted runtime access token (bypasses agent key)")
|
|
22
|
+
.option("--codex-bin <path>", "Path to codex binary (fallback to PATH)")
|
|
23
|
+
.option("--proxy-base-url <url>", "Codex proxy base URL")
|
|
24
|
+
.option("--workspace <path>", "Workspace directory (defaults to ./.instafy/workspace)")
|
|
25
|
+
.option("--origin-id <uuid>", "Origin ID to use (auto-generated if omitted)")
|
|
26
|
+
.option("--origin-endpoint <url>", "Explicit origin endpoint (skip tunnel setup when provided)")
|
|
27
|
+
.option("--origin-token <token>", "Runtime/origin access token for controller registration")
|
|
28
|
+
.option("--runtime-id <uuid>", "Runtime ID to use (bypass controller allocation)")
|
|
29
|
+
.option("--runtime-lease-id <uuid>", "Runtime lease ID to use (bypass controller allocation)")
|
|
30
|
+
.option("--display-name <name>", "Human-friendly runtime name")
|
|
31
|
+
.option("--provider <provider>", "Runtime provider label (default: self-hosted)")
|
|
32
|
+
.option("--bind-host <host>", "Origin bind host (default 127.0.0.1)")
|
|
33
|
+
.option("--bind-port <port>", "Origin bind port (default 54332)")
|
|
34
|
+
.option("--mount-controller-fs", "Mount controller workspace via SSHFS using mount-token API")
|
|
35
|
+
.option("--detach", "Run runtime in background and exit immediately")
|
|
36
|
+
.option("--log-file <path>", "Write runtime stdout/stderr to a file (implied when detached)")
|
|
37
|
+
.action(async (opts) => {
|
|
38
|
+
try {
|
|
39
|
+
await runtimeStart(opts);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error(kleur.red(String(error)));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command("runtime:status")
|
|
48
|
+
.description("Show runtime health (codex/proxy/controller/origin)")
|
|
49
|
+
.option("--json", "Output status as JSON")
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
try {
|
|
52
|
+
await runtimeStatus({ json: opts.json });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(kleur.red(String(error)));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command("runtime:sshfs:check")
|
|
61
|
+
.description("Verify sshfs availability and print platform-specific install hints")
|
|
62
|
+
.option("--sshfs-bin <path>", "sshfs binary (default: sshfs or SHARED_FS_BIN)")
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
try {
|
|
65
|
+
const { checkSshfs } = await import("./runtime.js");
|
|
66
|
+
await checkSshfs(opts.sshfsBin);
|
|
67
|
+
console.log(kleur.green(`sshfs OK (${opts.sshfsBin ?? process.env.SHARED_FS_BIN ?? "sshfs"})`));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error(kleur.red(String(error)));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
program
|
|
75
|
+
.command("runtime:stop")
|
|
76
|
+
.description("Stop the local Instafy runtime")
|
|
77
|
+
.option("--json", "Output result as JSON")
|
|
78
|
+
.action(async (opts) => {
|
|
79
|
+
try {
|
|
80
|
+
await runtimeStop({ json: opts.json });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error(kleur.red(String(error)));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
program
|
|
88
|
+
.command("runtime:token")
|
|
89
|
+
.description("Mint a runtime access token (unified agent/origin scopes)")
|
|
90
|
+
.requiredOption("--project <id>", "Project UUID")
|
|
91
|
+
.option("--controller-url <url>", "Controller base URL")
|
|
92
|
+
.option("--controller-access-token <token>", "Controller access token (required)")
|
|
93
|
+
.option("--runtime-id <uuid>", "Runtime ID to bind token to")
|
|
94
|
+
.option("--scope <scope...>", "Override scopes (default agent.* + origin.*)")
|
|
95
|
+
.option("--json", "Output token as JSON")
|
|
96
|
+
.action(async (opts) => {
|
|
97
|
+
try {
|
|
98
|
+
await runtimeToken({
|
|
99
|
+
project: opts.project,
|
|
100
|
+
controllerUrl: opts.controllerUrl,
|
|
101
|
+
controllerAccessToken: opts.controllerAccessToken,
|
|
102
|
+
runtimeId: opts.runtimeId,
|
|
103
|
+
scopes: opts.scope,
|
|
104
|
+
json: opts.json,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error(kleur.red(String(error)));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
program
|
|
113
|
+
.command("project:init")
|
|
114
|
+
.description("Create a project via the controller and write .instafy/project.json in the target path")
|
|
115
|
+
.option("--path <dir>", "Directory where the manifest should be written (default: cwd)")
|
|
116
|
+
.option("--controller-url <url>", "Controller base URL")
|
|
117
|
+
.option("--access-token <token>", "Controller or Supabase access token")
|
|
118
|
+
.option("--project-type <type>", "Project type (customer|sandbox)")
|
|
119
|
+
.option("--org-id <uuid>", "Optional organization id")
|
|
120
|
+
.option("--org-name <name>", "Optional organization name")
|
|
121
|
+
.option("--org-slug <slug>", "Optional organization slug")
|
|
122
|
+
.option("--owner-user-id <uuid>", "Explicit owner user id (defaults to caller)")
|
|
123
|
+
.option("--json", "Output JSON")
|
|
124
|
+
.action(async (opts) => {
|
|
125
|
+
try {
|
|
126
|
+
await projectInit({
|
|
127
|
+
path: opts.path,
|
|
128
|
+
controllerUrl: opts.controllerUrl,
|
|
129
|
+
accessToken: opts.accessToken,
|
|
130
|
+
projectType: opts.projectType,
|
|
131
|
+
orgId: opts.orgId,
|
|
132
|
+
orgName: opts.orgName,
|
|
133
|
+
orgSlug: opts.orgSlug,
|
|
134
|
+
ownerUserId: opts.ownerUserId,
|
|
135
|
+
json: opts.json,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.error(kleur.red(String(error)));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
program
|
|
144
|
+
.command("tunnel")
|
|
145
|
+
.description("Request a tunnel from the controller and forward a local port via rathole")
|
|
146
|
+
.option("--project <id>", "Project UUID (or set PROJECT_ID)")
|
|
147
|
+
.option("--controller-url <url>", "Controller base URL (or CONTROLLER_URL)")
|
|
148
|
+
.option("--controller-token <token>", "Controller access token (service-role/internal)")
|
|
149
|
+
.option("--port <port>", "Local port to expose (default 3000)")
|
|
150
|
+
.option("--rathole-bin <path>", "Path to rathole binary (or set RATHOLE_BIN)")
|
|
151
|
+
.action(async (opts) => {
|
|
152
|
+
try {
|
|
153
|
+
const port = opts.port ? Number(opts.port) : undefined;
|
|
154
|
+
await runTunnelCommand({
|
|
155
|
+
project: opts.project,
|
|
156
|
+
controllerUrl: opts.controllerUrl,
|
|
157
|
+
controllerToken: opts.controllerToken,
|
|
158
|
+
port,
|
|
159
|
+
ratholeBin: opts.ratholeBin,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
console.error(kleur.red(String(error)));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
program
|
|
168
|
+
.command("org:list")
|
|
169
|
+
.description("List organizations accessible to the current access token")
|
|
170
|
+
.option("--controller-url <url>", "Controller base URL")
|
|
171
|
+
.option("--access-token <token>", "Controller or Supabase access token")
|
|
172
|
+
.option("--json", "Output JSON")
|
|
173
|
+
.action(async (opts) => {
|
|
174
|
+
try {
|
|
175
|
+
await (await import("./org.js")).listOrganizations({
|
|
176
|
+
controllerUrl: opts.controllerUrl,
|
|
177
|
+
accessToken: opts.accessToken,
|
|
178
|
+
json: opts.json,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error(kleur.red(String(error)));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
program
|
|
187
|
+
.command("project:list")
|
|
188
|
+
.description("List projects in your organizations")
|
|
189
|
+
.option("--controller-url <url>", "Controller base URL")
|
|
190
|
+
.option("--access-token <token>", "Controller or Supabase access token")
|
|
191
|
+
.option("--org-id <uuid>", "Filter by organization id")
|
|
192
|
+
.option("--org-slug <slug>", "Filter by organization slug")
|
|
193
|
+
.option("--json", "Output JSON")
|
|
194
|
+
.action(async (opts) => {
|
|
195
|
+
try {
|
|
196
|
+
await (await import("./project.js")).listProjects({
|
|
197
|
+
controllerUrl: opts.controllerUrl,
|
|
198
|
+
accessToken: opts.accessToken,
|
|
199
|
+
orgId: opts.orgId,
|
|
200
|
+
orgSlug: opts.orgSlug,
|
|
201
|
+
json: opts.json,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error(kleur.red(String(error)));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
program
|
|
210
|
+
.command("workspace:mount")
|
|
211
|
+
.description("Mount the controller-managed workspace locally over SSHFS")
|
|
212
|
+
.requiredOption("--project <id>", "Project UUID")
|
|
213
|
+
.requiredOption("--path <dir>", "Local mount directory")
|
|
214
|
+
.option("--controller-url <url>", "Controller base URL", "http://127.0.0.1:8788")
|
|
215
|
+
.option("--controller-access-token <token>", "Controller access token (service-role or project member)")
|
|
216
|
+
.option("--sshfs-bin <path>", "sshfs binary (default: sshfs or SHARED_FS_BIN)")
|
|
217
|
+
.option("--options <opts>", "Extra sshfs options")
|
|
218
|
+
.action(async (opts) => {
|
|
219
|
+
try {
|
|
220
|
+
const token = opts.controllerAccessToken ??
|
|
221
|
+
process.env.CONTROLLER_ACCESS_TOKEN ??
|
|
222
|
+
process.env.CONTROLLER_TOKEN;
|
|
223
|
+
if (!token) {
|
|
224
|
+
throw new Error("controller access token is required for workspace:mount");
|
|
225
|
+
}
|
|
226
|
+
const mountDir = await mountWorkspace({
|
|
227
|
+
project: opts.project,
|
|
228
|
+
controllerUrl: opts.controllerUrl,
|
|
229
|
+
controllerAccessToken: token,
|
|
230
|
+
mountPath: opts.path,
|
|
231
|
+
sshfsBin: opts.sshfsBin,
|
|
232
|
+
extraOptions: opts.options,
|
|
233
|
+
});
|
|
234
|
+
console.log(kleur.green(`Mounted controller workspace to ${mountDir}`));
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.error(kleur.red(String(error)));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
242
|
+
program.parseAsync(process.argv);
|
|
243
|
+
}
|
|
244
|
+
// Re-export programmatic APIs for embedders (e.g., VS Code extension).
|
|
245
|
+
export { runtimeStart as startRuntime, runtimeStatus as getRuntimeStatus, runtimeStop as stopRuntime, runtimeToken as mintRuntimeToken, mintRuntimeAccessToken, findProjectManifest, mountWorkspace, } from "./runtime.js";
|
|
246
|
+
export { projectInit, listProjects } from "./project.js";
|
|
247
|
+
export { listOrganizations } from "./org.js";
|
package/dist/org.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
function normalizeUrl(raw) {
|
|
3
|
+
const value = (raw ?? "").trim();
|
|
4
|
+
if (!value)
|
|
5
|
+
return "http://127.0.0.1:8788";
|
|
6
|
+
return value.replace(/\/$/, "");
|
|
7
|
+
}
|
|
8
|
+
function normalizeToken(raw) {
|
|
9
|
+
if (!raw)
|
|
10
|
+
return null;
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
return trimmed.length ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
export async function listOrganizations(params) {
|
|
15
|
+
const controllerUrl = normalizeUrl(params.controllerUrl ?? process.env["CONTROLLER_BASE_URL"]);
|
|
16
|
+
const token = normalizeToken(params.accessToken) ??
|
|
17
|
+
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
18
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
19
|
+
if (!token) {
|
|
20
|
+
throw new Error("Controller access token is required (--access-token, CONTROLLER_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
|
|
21
|
+
}
|
|
22
|
+
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
23
|
+
headers: { authorization: `Bearer ${token}` },
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const text = await response.text().catch(() => "");
|
|
27
|
+
throw new Error(`Organization list failed (${response.status} ${response.statusText}): ${text}`);
|
|
28
|
+
}
|
|
29
|
+
const body = (await response.json());
|
|
30
|
+
const orgs = Array.isArray(body.orgs) ? body.orgs : [];
|
|
31
|
+
if (params.json) {
|
|
32
|
+
console.log(JSON.stringify(orgs, null, 2));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (orgs.length === 0) {
|
|
36
|
+
console.log(kleur.yellow("No organizations found for this account."));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
for (const org of orgs) {
|
|
40
|
+
const role = org.role ? ` [${org.role}]` : "";
|
|
41
|
+
console.log(`${kleur.green(org.name)} (${org.id}${role})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return orgs;
|
|
46
|
+
}
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
function normalizeUrl(raw) {
|
|
5
|
+
const value = (raw ?? "").trim();
|
|
6
|
+
if (!value)
|
|
7
|
+
return "http://127.0.0.1:8788";
|
|
8
|
+
return value.replace(/\/$/, "");
|
|
9
|
+
}
|
|
10
|
+
function normalizeToken(raw) {
|
|
11
|
+
if (!raw)
|
|
12
|
+
return null;
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
return trimmed.length ? trimmed : null;
|
|
15
|
+
}
|
|
16
|
+
async function fetchOrganizations(controllerUrl, token) {
|
|
17
|
+
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
18
|
+
headers: {
|
|
19
|
+
authorization: `Bearer ${token}`,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const text = await response.text().catch(() => "");
|
|
24
|
+
throw new Error(`Organization list failed (${response.status} ${response.statusText}): ${text}`);
|
|
25
|
+
}
|
|
26
|
+
const body = (await response.json());
|
|
27
|
+
return Array.isArray(body.orgs) ? body.orgs : [];
|
|
28
|
+
}
|
|
29
|
+
async function fetchOrgProjects(controllerUrl, token, orgId) {
|
|
30
|
+
const response = await fetch(`${controllerUrl}/orgs/${encodeURIComponent(orgId)}/projects`, {
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: `Bearer ${token}`,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const text = await response.text().catch(() => "");
|
|
37
|
+
throw new Error(`Project list failed (${response.status} ${response.statusText}): ${text}`);
|
|
38
|
+
}
|
|
39
|
+
const body = (await response.json());
|
|
40
|
+
return Array.isArray(body.projects) ? body.projects : [];
|
|
41
|
+
}
|
|
42
|
+
async function resolveOrg(controllerUrl, token, options) {
|
|
43
|
+
if (options.orgId) {
|
|
44
|
+
return { orgId: options.orgId, orgName: options.orgName ?? null };
|
|
45
|
+
}
|
|
46
|
+
const payload = {};
|
|
47
|
+
if (options.orgName) {
|
|
48
|
+
payload.orgName = options.orgName;
|
|
49
|
+
}
|
|
50
|
+
if (options.orgSlug) {
|
|
51
|
+
payload.orgSlug = options.orgSlug;
|
|
52
|
+
}
|
|
53
|
+
if (options.ownerUserId) {
|
|
54
|
+
payload.ownerUserId = options.ownerUserId;
|
|
55
|
+
}
|
|
56
|
+
const response = await fetch(`${controllerUrl}/orgs`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
authorization: `Bearer ${token}`,
|
|
60
|
+
"content-type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(payload),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text().catch(() => "");
|
|
66
|
+
throw new Error(`Organization creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
67
|
+
}
|
|
68
|
+
const json = (await response.json());
|
|
69
|
+
const orgId = json.org_id;
|
|
70
|
+
if (!orgId) {
|
|
71
|
+
throw new Error("Organization creation response missing org_id");
|
|
72
|
+
}
|
|
73
|
+
return { orgId, orgName: json.org_name ?? null };
|
|
74
|
+
}
|
|
75
|
+
export async function listProjects(options) {
|
|
76
|
+
const controllerUrl = normalizeUrl(options.controllerUrl ?? process.env["CONTROLLER_BASE_URL"]);
|
|
77
|
+
const token = normalizeToken(options.accessToken) ??
|
|
78
|
+
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
79
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
80
|
+
if (!token) {
|
|
81
|
+
throw new Error("Controller or Supabase access token is required (--access-token, CONTROLLER_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
|
|
82
|
+
}
|
|
83
|
+
const orgs = await fetchOrganizations(controllerUrl, token);
|
|
84
|
+
let targetOrgs = orgs;
|
|
85
|
+
if (options.orgId) {
|
|
86
|
+
targetOrgs = orgs.filter((org) => org.id === options.orgId);
|
|
87
|
+
if (targetOrgs.length === 0) {
|
|
88
|
+
throw new Error(`No organization found for id ${options.orgId}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (options.orgSlug) {
|
|
92
|
+
targetOrgs = orgs.filter((org) => org.slug === options.orgSlug);
|
|
93
|
+
if (targetOrgs.length === 0) {
|
|
94
|
+
throw new Error(`No organization found for slug ${options.orgSlug}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const summaries = [];
|
|
98
|
+
for (const org of targetOrgs) {
|
|
99
|
+
const projects = await fetchOrgProjects(controllerUrl, token, org.id);
|
|
100
|
+
summaries.push({ org, projects });
|
|
101
|
+
}
|
|
102
|
+
if (options.json) {
|
|
103
|
+
console.log(JSON.stringify(summaries, null, 2));
|
|
104
|
+
}
|
|
105
|
+
else if (summaries.length === 0) {
|
|
106
|
+
console.log(kleur.yellow("No organizations found for this account."));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
for (const summary of summaries) {
|
|
110
|
+
console.log(`${kleur.green(summary.org.name)} (${summary.org.id})`);
|
|
111
|
+
if (summary.projects.length === 0) {
|
|
112
|
+
console.log(kleur.yellow(" No projects found."));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const project of summary.projects) {
|
|
116
|
+
const type = project.project_type ? ` · ${project.project_type}` : "";
|
|
117
|
+
const status = project.status ? ` · ${project.status}` : "";
|
|
118
|
+
console.log(` ${project.project_id}${type}${status}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return summaries.flatMap((summary) => summary.projects);
|
|
123
|
+
}
|
|
124
|
+
export async function projectInit(options) {
|
|
125
|
+
const rootDir = path.resolve(options.path ?? process.cwd());
|
|
126
|
+
const controllerUrl = normalizeUrl(options.controllerUrl ?? process.env["CONTROLLER_BASE_URL"]);
|
|
127
|
+
const token = normalizeToken(options.accessToken) ??
|
|
128
|
+
normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"]) ??
|
|
129
|
+
normalizeToken(process.env["SUPABASE_ACCESS_TOKEN"]);
|
|
130
|
+
if (!token) {
|
|
131
|
+
throw new Error("Controller or Supabase access token is required (--access-token, CONTROLLER_ACCESS_TOKEN, or SUPABASE_ACCESS_TOKEN).");
|
|
132
|
+
}
|
|
133
|
+
const org = await resolveOrg(controllerUrl, token, options);
|
|
134
|
+
const body = {
|
|
135
|
+
projectType: options.projectType,
|
|
136
|
+
ownerUserId: options.ownerUserId,
|
|
137
|
+
};
|
|
138
|
+
const response = await fetch(`${controllerUrl}/orgs/${encodeURIComponent(org.orgId)}/projects`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
authorization: `Bearer ${token}`,
|
|
142
|
+
"content-type": "application/json",
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const text = await response.text().catch(() => "");
|
|
148
|
+
throw new Error(`Project creation failed (${response.status} ${response.statusText}): ${text}`);
|
|
149
|
+
}
|
|
150
|
+
const json = (await response.json());
|
|
151
|
+
const manifestDir = path.join(rootDir, ".instafy");
|
|
152
|
+
const manifestPath = path.join(manifestDir, "project.json");
|
|
153
|
+
try {
|
|
154
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
155
|
+
fs.writeFileSync(manifestPath, JSON.stringify({
|
|
156
|
+
projectId: json.project_id,
|
|
157
|
+
orgId: json.org_id ?? org.orgId ?? null,
|
|
158
|
+
orgName: json.org_name ?? org.orgName ?? null,
|
|
159
|
+
controllerUrl,
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
}, null, 2), "utf8");
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new Error(`Project created but failed to write manifest at ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
if (options.json) {
|
|
167
|
+
console.log(JSON.stringify({
|
|
168
|
+
projectId: json.project_id,
|
|
169
|
+
orgId: json.org_id ?? org.orgId ?? null,
|
|
170
|
+
orgName: json.org_name ?? org.orgName ?? null,
|
|
171
|
+
manifest: manifestPath,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(kleur.green(`Project created: ${json.project_id}`));
|
|
176
|
+
const resolvedOrgName = json.org_name ?? org.orgName;
|
|
177
|
+
const resolvedOrgId = json.org_id ?? org.orgId;
|
|
178
|
+
if (resolvedOrgName) {
|
|
179
|
+
console.log(kleur.cyan(`Org: ${resolvedOrgName}${resolvedOrgId ? ` (${resolvedOrgId})` : ""}`));
|
|
180
|
+
}
|
|
181
|
+
console.log(kleur.cyan(`Manifest written to ${manifestPath}`));
|
|
182
|
+
}
|
|
183
|
+
return json;
|
|
184
|
+
}
|