iterate 0.2.0 → 0.2.3
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 +12 -12
- package/bin/iterate.js +395 -438
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,11 +29,10 @@ Initial setup (writes auth + launcher config):
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npx iterate setup \
|
|
32
|
-
--base-url https://dev-yourname-os.dev.iterate.com \
|
|
32
|
+
--os-base-url https://dev-yourname-os.dev.iterate.com \
|
|
33
|
+
--daemon-base-url http://localhost:3001 \
|
|
33
34
|
--admin-password-env-var-name SERVICE_AUTH_TOKEN \
|
|
34
|
-
--user-
|
|
35
|
-
--repo-path managed \
|
|
36
|
-
--auto-install true \
|
|
35
|
+
--user-email dev-yourname@iterate.com \
|
|
37
36
|
--scope global
|
|
38
37
|
```
|
|
39
38
|
|
|
@@ -51,6 +50,9 @@ npx iterate os project list
|
|
|
51
50
|
- `iterate install` - force clone/install for resolved checkout
|
|
52
51
|
- `iterate whoami`
|
|
53
52
|
- `iterate os ...`
|
|
53
|
+
- `iterate daemon ...`
|
|
54
|
+
|
|
55
|
+
`setup --scope global` writes auth + launcher values into `global`; `setup --scope workspace` writes them into `workspaces[process.cwd()]`.
|
|
54
56
|
|
|
55
57
|
## Config file
|
|
56
58
|
|
|
@@ -70,11 +72,10 @@ Config shape:
|
|
|
70
72
|
},
|
|
71
73
|
"workspaces": {
|
|
72
74
|
"/absolute/workspace/path": {
|
|
73
|
-
"
|
|
75
|
+
"osBaseUrl": "https://dev-yourname-os.dev.iterate.com",
|
|
76
|
+
"daemonBaseUrl": "http://localhost:3001",
|
|
74
77
|
"adminPasswordEnvVarName": "SERVICE_AUTH_TOKEN",
|
|
75
|
-
"
|
|
76
|
-
"repoPath": "/absolute/path/to/iterate",
|
|
77
|
-
"autoInstall": false
|
|
78
|
+
"userEmail": "dev-yourname@iterate.com"
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -114,11 +115,10 @@ You can pin explicitly:
|
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
117
|
npx iterate setup \
|
|
117
|
-
--base-url https://dev-yourname-os.dev.iterate.com \
|
|
118
|
+
--os-base-url https://dev-yourname-os.dev.iterate.com \
|
|
119
|
+
--daemon-base-url http://localhost:3001 \
|
|
118
120
|
--admin-password-env-var-name SERVICE_AUTH_TOKEN \
|
|
119
|
-
--user-
|
|
120
|
-
--repo-path local \
|
|
121
|
-
--auto-install false \
|
|
121
|
+
--user-email dev-yourname@iterate.com \
|
|
122
122
|
--scope workspace
|
|
123
123
|
```
|
|
124
124
|
|
package/bin/iterate.js
CHANGED
|
@@ -1,66 +1,130 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { dirname,
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { initTRPC } from "@trpc/server";
|
|
13
|
-
import { createAuthClient } from "better-auth/client";
|
|
14
|
-
import { adminClient } from "better-auth/client/plugins";
|
|
15
|
-
import superjson from "superjson";
|
|
16
|
-
import { createCli } from "trpc-cli";
|
|
17
|
-
import { proxify } from "trpc-cli/dist/proxify.js";
|
|
18
|
-
import { z } from "zod/v4";
|
|
19
|
-
|
|
20
|
-
const DEFAULT_REPO_URL = "https://github.com/iterate/iterate.git";
|
|
21
|
-
const XDG_CONFIG_PATH = join(
|
|
22
|
-
process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config"),
|
|
23
|
-
"iterate",
|
|
24
|
-
"config.json",
|
|
25
|
-
);
|
|
26
|
-
const XDG_REPO_DIR = join(
|
|
27
|
-
process.env.XDG_DATA_HOME ? process.env.XDG_DATA_HOME : join(homedir(), ".local", "share"),
|
|
28
|
-
"iterate",
|
|
29
|
-
"repo",
|
|
30
|
-
);
|
|
31
|
-
const DEFAULT_REPO_DIR = XDG_REPO_DIR;
|
|
32
|
-
const CONFIG_PATH = XDG_CONFIG_PATH;
|
|
33
|
-
const APP_ROUTER_PATH = join("apps", "os", "backend", "trpc", "root.ts");
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// --- Local delegation (must run before any heavy imports) ---
|
|
34
12
|
|
|
35
13
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* }} LauncherConfig
|
|
14
|
+
* Walk up from `startDir` looking for `relativePath` to exist.
|
|
15
|
+
* Returns the directory where it was found, or null.
|
|
16
|
+
* @param {string} relativePath
|
|
17
|
+
* @param {string} [startDir]
|
|
18
|
+
* @returns {string | null}
|
|
42
19
|
*/
|
|
20
|
+
const findUp = (relativePath, startDir = process.cwd()) => {
|
|
21
|
+
let dir = resolve(startDir);
|
|
22
|
+
while (true) {
|
|
23
|
+
if (existsSync(join(dir, relativePath))) return dir;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) return null;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
31
|
|
|
44
32
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* workspaces?: Record<string, Record<string, unknown>>;
|
|
48
|
-
* } & Record<string, unknown>} ConfigFile
|
|
33
|
+
* If we're already running from a local version, skip delegation to avoid loops.
|
|
34
|
+
* Otherwise, find the closest local iterate CLI and re-exec into it.
|
|
49
35
|
*/
|
|
36
|
+
const delegateToLocal = () => {
|
|
37
|
+
if (process.env.__ITERATE_CLI_DELEGATED) return;
|
|
38
|
+
|
|
39
|
+
const selfReal = realpathSync(__filename);
|
|
40
|
+
|
|
41
|
+
// 1. Check if we're inside the iterate repo (has pnpm-workspace.yaml at root)
|
|
42
|
+
const repoRoot = findUp("pnpm-workspace.yaml");
|
|
43
|
+
if (repoRoot) {
|
|
44
|
+
const repoScript = join(repoRoot, "packages/iterate/bin/iterate.js");
|
|
45
|
+
if (existsSync(repoScript) && realpathSync(repoScript) !== selfReal) {
|
|
46
|
+
reExec(repoScript);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// 2. Check for a local node_modules install
|
|
52
|
+
const nmRoot = findUp("node_modules/.bin/iterate");
|
|
53
|
+
if (nmRoot) {
|
|
54
|
+
const nmScript = join(nmRoot, "node_modules/.bin/iterate");
|
|
55
|
+
if (existsSync(nmScript) && realpathSync(nmScript) !== selfReal) {
|
|
56
|
+
reExec(nmScript);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* repoDirSource: RepoDirSource;
|
|
57
|
-
* repoRef?: string;
|
|
58
|
-
* repoUrl: string;
|
|
59
|
-
* autoInstall: boolean;
|
|
60
|
-
* cwdRepoDir?: string;
|
|
61
|
-
* launcherConfig: LauncherConfig;
|
|
62
|
-
* }} RuntimeOptions
|
|
63
|
+
* Re-exec into `scriptPath` with the same argv, never returning.
|
|
64
|
+
* @param {string} scriptPath
|
|
63
65
|
*/
|
|
66
|
+
const reExec = (scriptPath) => {
|
|
67
|
+
try {
|
|
68
|
+
execFileSync(process.execPath, [scriptPath, ...process.argv.slice(2)], {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: { ...process.env, __ITERATE_CLI_DELEGATED: "1" },
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
process.exit(e && typeof e === "object" && "status" in e ? Number(e.status) || 1 : 1);
|
|
74
|
+
}
|
|
75
|
+
process.exit(0);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
delegateToLocal();
|
|
79
|
+
|
|
80
|
+
// --- Normal CLI startup (dynamic imports so delegation can short-circuit first) ---
|
|
81
|
+
|
|
82
|
+
const prompts = await import("@clack/prompts");
|
|
83
|
+
const { createTRPCClient, httpLink } = await import("@trpc/client");
|
|
84
|
+
const { initTRPC } = await import("@trpc/server");
|
|
85
|
+
const { createAuthClient } = await import("better-auth/client");
|
|
86
|
+
const { adminClient } = await import("better-auth/client/plugins");
|
|
87
|
+
const { default: superjson } = await import("superjson");
|
|
88
|
+
const { createCli } = await import("trpc-cli");
|
|
89
|
+
const { proxify } = await import("trpc-cli/dist/proxify.js");
|
|
90
|
+
const { z } = await import("zod/v4");
|
|
91
|
+
|
|
92
|
+
const XDG_CONFIG_PARENT = join(
|
|
93
|
+
process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config"),
|
|
94
|
+
"iterate",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const XDG_CONFIG_PATH = join(XDG_CONFIG_PARENT, "config.json");
|
|
98
|
+
const CONFIG_PATH = XDG_CONFIG_PATH;
|
|
99
|
+
// todo write json schema to file too - need to make everything zod first
|
|
100
|
+
// const CONFIG_SCHEMA_PATH = join(XDG_CONFIG_PARENT, "config-schema.json");
|
|
101
|
+
|
|
102
|
+
const SetupInput = z.object({
|
|
103
|
+
osBaseUrl: z
|
|
104
|
+
.string()
|
|
105
|
+
.describe(`Base URL for OS API (for example https://dev-yourname-os.dev.iterate.com)`),
|
|
106
|
+
daemonBaseUrl: z.string().describe(`Base URL for daemon API (for example http://localhost:3001)`),
|
|
107
|
+
adminPasswordEnvVarName: z.string().describe("Env var name containing admin password"),
|
|
108
|
+
userEmail: z.string().describe("User email to impersonate for OS calls"),
|
|
109
|
+
scope: z.enum(["workspace", "global"]).describe("Where to store launcher config"),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const AuthConfig = z.object({
|
|
113
|
+
osBaseUrl: z.string(),
|
|
114
|
+
daemonBaseUrl: z.string(),
|
|
115
|
+
adminPasswordEnvVarName: z.string(),
|
|
116
|
+
userEmail: z.string(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const ConfigFile = z.object({
|
|
120
|
+
global: AuthConfig.partial().optional(),
|
|
121
|
+
workspaces: z.record(z.string(), AuthConfig).optional(),
|
|
122
|
+
/** a place where I put old/invalid configs I can't quite let go of */
|
|
123
|
+
rubbish: z.unknown().optional(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** @typedef {import('zod').infer<typeof AuthConfig>} AuthConfig */
|
|
127
|
+
/** @typedef {import('zod').infer<typeof ConfigFile>} ConfigFile */
|
|
64
128
|
|
|
65
129
|
/**
|
|
66
130
|
* @typedef {{
|
|
@@ -71,14 +135,6 @@ const APP_ROUTER_PATH = join("apps", "os", "backend", "trpc", "root.ts");
|
|
|
71
135
|
* }} SpawnOptions
|
|
72
136
|
*/
|
|
73
137
|
|
|
74
|
-
/**
|
|
75
|
-
* @typedef {{
|
|
76
|
-
* repoDir: string;
|
|
77
|
-
* repoRef?: string;
|
|
78
|
-
* repoUrl: string;
|
|
79
|
-
* }} CheckoutOptions
|
|
80
|
-
*/
|
|
81
|
-
|
|
82
138
|
const isAgent =
|
|
83
139
|
process.env.AGENT === "1" ||
|
|
84
140
|
process.env.OPENCODE === "1" ||
|
|
@@ -87,28 +143,6 @@ const isAgent =
|
|
|
87
143
|
|
|
88
144
|
const t = initTRPC.meta().create();
|
|
89
145
|
|
|
90
|
-
const SetupInput = z.object({
|
|
91
|
-
baseUrl: z
|
|
92
|
-
.string()
|
|
93
|
-
.describe(`Base URL for os API (for example https://dev-yourname-os.dev.iterate.com)`),
|
|
94
|
-
adminPasswordEnvVarName: z.string().describe("Env var name containing admin password"),
|
|
95
|
-
userId: z.string().describe("User ID to impersonate for os calls"),
|
|
96
|
-
repoPath: z.string().describe("Path to iterate checkout (or 'local' / 'managed' shortcuts)"),
|
|
97
|
-
autoInstall: z.boolean().describe("Auto install dependencies when missing"),
|
|
98
|
-
scope: z.enum(["workspace", "global"]).describe("Where to store launcher config"),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const AuthConfig = z.object({
|
|
102
|
-
baseUrl: z.string(),
|
|
103
|
-
adminPasswordEnvVarName: z.string(),
|
|
104
|
-
userId: z.string(),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/** @param {string} message */
|
|
108
|
-
const log = (message) => {
|
|
109
|
-
process.stderr.write(`[iterate] ${message}\n`);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
146
|
/**
|
|
113
147
|
* @param {unknown} value
|
|
114
148
|
* @returns {value is Record<string, unknown>}
|
|
@@ -117,44 +151,6 @@ const isObject = (value) => {
|
|
|
117
151
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
118
152
|
};
|
|
119
153
|
|
|
120
|
-
/** @param {unknown} value */
|
|
121
|
-
const nonEmptyString = (value) => {
|
|
122
|
-
if (typeof value !== "string") {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
const trimmed = value.trim();
|
|
126
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/** @param {string} input */
|
|
130
|
-
const normalizePath = (input) => {
|
|
131
|
-
if (input === "~") {
|
|
132
|
-
return homedir();
|
|
133
|
-
}
|
|
134
|
-
if (input.startsWith("~/")) {
|
|
135
|
-
return join(homedir(), input.slice(2));
|
|
136
|
-
}
|
|
137
|
-
if (isAbsolute(input)) {
|
|
138
|
-
return input;
|
|
139
|
-
}
|
|
140
|
-
return resolve(input);
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
/** @param {unknown} value */
|
|
144
|
-
const parseBoolean = (value) => {
|
|
145
|
-
if (typeof value !== "string") {
|
|
146
|
-
return undefined;
|
|
147
|
-
}
|
|
148
|
-
const normalized = value.toLowerCase();
|
|
149
|
-
if (normalized === "1" || normalized === "true") {
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
if (normalized === "0" || normalized === "false") {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
154
|
/** @returns {ConfigFile} */
|
|
159
155
|
const readConfigFile = () => {
|
|
160
156
|
if (!existsSync(CONFIG_PATH)) {
|
|
@@ -169,30 +165,9 @@ const readConfigFile = () => {
|
|
|
169
165
|
throw new Error(`Invalid JSON in ${CONFIG_PATH}: ${detail}`);
|
|
170
166
|
}
|
|
171
167
|
|
|
172
|
-
if (!isObject(parsed)) {
|
|
173
|
-
throw new Error(`${CONFIG_PATH} must contain a JSON object.`);
|
|
174
|
-
}
|
|
175
168
|
return parsed;
|
|
176
169
|
};
|
|
177
170
|
|
|
178
|
-
/** @param {unknown} launcher */
|
|
179
|
-
const sanitizeLauncherConfig = (launcher) => {
|
|
180
|
-
if (!isObject(launcher)) {
|
|
181
|
-
return {};
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
repoPath: nonEmptyString(launcher.repoPath),
|
|
185
|
-
repoRef: nonEmptyString(launcher.repoRef),
|
|
186
|
-
repoUrl: nonEmptyString(launcher.repoUrl),
|
|
187
|
-
autoInstall: typeof launcher.autoInstall === "boolean" ? launcher.autoInstall : undefined,
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/** @param {ConfigFile} configFile */
|
|
192
|
-
const getGlobalConfig = (configFile) => {
|
|
193
|
-
return isObject(configFile.global) ? configFile.global : {};
|
|
194
|
-
};
|
|
195
|
-
|
|
196
171
|
/**
|
|
197
172
|
* @param {ConfigFile} configFile
|
|
198
173
|
* @param {string} workspacePath
|
|
@@ -209,119 +184,42 @@ const getWorkspaceConfig = (configFile, workspacePath) => {
|
|
|
209
184
|
*/
|
|
210
185
|
const getMergedWorkspaceConfig = (configFile, workspacePath) => {
|
|
211
186
|
return {
|
|
212
|
-
...
|
|
187
|
+
...configFile.global,
|
|
213
188
|
...getWorkspaceConfig(configFile, workspacePath),
|
|
214
189
|
};
|
|
215
190
|
};
|
|
216
191
|
|
|
217
|
-
/** @param {string} workspacePath */
|
|
218
|
-
const readLauncherConfig = (workspacePath) => {
|
|
219
|
-
const configFile = readConfigFile();
|
|
220
|
-
return sanitizeLauncherConfig(getMergedWorkspaceConfig(configFile, workspacePath));
|
|
221
|
-
};
|
|
222
|
-
|
|
223
192
|
/**
|
|
224
|
-
* @param {{
|
|
225
|
-
*
|
|
226
|
-
* workspacePatch?: Record<string, unknown>;
|
|
227
|
-
* scope: "workspace" | "global";
|
|
228
|
-
* workspacePath: string;
|
|
229
|
-
* }} options
|
|
193
|
+
* @param {{ patch?: Partial<AuthConfig>; scope: "workspace" | "global"; workspacePath: string; }} options
|
|
194
|
+
* @returns {ConfigFile}
|
|
230
195
|
*/
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const existingWorkspaces = isObject(configFile.workspaces) ? configFile.workspaces : {};
|
|
235
|
-
|
|
236
|
-
const nextGlobal = scope === "global" ? { ...existingGlobal, ...launcherPatch } : existingGlobal;
|
|
237
|
-
const nextWorkspaces =
|
|
238
|
-
scope === "workspace" || workspacePatch
|
|
239
|
-
? {
|
|
240
|
-
...existingWorkspaces,
|
|
241
|
-
[workspacePath]: {
|
|
242
|
-
...getWorkspaceConfig(configFile, workspacePath),
|
|
243
|
-
...(scope === "workspace" ? launcherPatch : {}),
|
|
244
|
-
...(workspacePatch ?? {}),
|
|
245
|
-
},
|
|
246
|
-
}
|
|
247
|
-
: existingWorkspaces;
|
|
248
|
-
|
|
249
|
-
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
250
|
-
const { launcher: _unusedLauncher, ...rest } = configFile;
|
|
251
|
-
const next = {
|
|
252
|
-
...rest,
|
|
253
|
-
global: nextGlobal,
|
|
254
|
-
workspaces: nextWorkspaces,
|
|
255
|
-
};
|
|
256
|
-
writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
|
257
|
-
return next;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
/** @param {string} dir */
|
|
261
|
-
const isIterateRepo = (dir) => {
|
|
262
|
-
return (
|
|
263
|
-
existsSync(join(dir, ".git")) &&
|
|
264
|
-
existsSync(join(dir, "pnpm-workspace.yaml")) &&
|
|
265
|
-
existsSync(join(dir, APP_ROUTER_PATH))
|
|
196
|
+
const writeNewConfig = ({ patch, scope, workspacePath }) => {
|
|
197
|
+
patch = Object.fromEntries(
|
|
198
|
+
Object.entries(patch || {}).filter(([_key, value]) => value !== undefined),
|
|
266
199
|
);
|
|
267
|
-
|
|
200
|
+
const configFile = readConfigFile();
|
|
201
|
+
const cloned = structuredClone(configFile);
|
|
268
202
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let current = resolve(startDir);
|
|
272
|
-
for (;;) {
|
|
273
|
-
if (isIterateRepo(current)) {
|
|
274
|
-
return current;
|
|
275
|
-
}
|
|
276
|
-
const parent = dirname(current);
|
|
277
|
-
if (parent === current) {
|
|
278
|
-
return undefined;
|
|
279
|
-
}
|
|
280
|
-
current = parent;
|
|
203
|
+
if (scope === "global") {
|
|
204
|
+
cloned.global = { ...configFile.global, ...patch };
|
|
281
205
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
let repoDir;
|
|
291
|
-
/** @type {RepoDirSource} */
|
|
292
|
-
let repoDirSource;
|
|
293
|
-
|
|
294
|
-
if (envRepoDir) {
|
|
295
|
-
repoDir = normalizePath(envRepoDir);
|
|
296
|
-
repoDirSource = "env";
|
|
297
|
-
} else if (launcherConfig.repoPath) {
|
|
298
|
-
repoDir = normalizePath(launcherConfig.repoPath);
|
|
299
|
-
repoDirSource = "config";
|
|
300
|
-
} else if (cwdRepoDir) {
|
|
301
|
-
repoDir = cwdRepoDir;
|
|
302
|
-
repoDirSource = "cwd";
|
|
303
|
-
} else {
|
|
304
|
-
repoDir = DEFAULT_REPO_DIR;
|
|
305
|
-
repoDirSource = "default";
|
|
206
|
+
if (scope === "workspace" && workspacePath) {
|
|
207
|
+
cloned.workspaces ||= {};
|
|
208
|
+
// @ts-expect-error - we know it's a string
|
|
209
|
+
cloned.workspaces[workspacePath] = {
|
|
210
|
+
...configFile.workspaces?.[workspacePath],
|
|
211
|
+
...patch,
|
|
212
|
+
};
|
|
306
213
|
}
|
|
307
214
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
parseBoolean(process.env.ITERATE_AUTO_INSTALL) ??
|
|
313
|
-
launcherConfig.autoInstall ??
|
|
314
|
-
(repoDirSource === "cwd" ? false : true);
|
|
215
|
+
const parsed = ConfigFile.safeParse(cloned);
|
|
216
|
+
if (!parsed.success) {
|
|
217
|
+
throw new Error(`Invalid config file: ${z.prettifyError(parsed.error)}`);
|
|
218
|
+
}
|
|
315
219
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
repoRef,
|
|
320
|
-
repoUrl,
|
|
321
|
-
autoInstall,
|
|
322
|
-
cwdRepoDir,
|
|
323
|
-
launcherConfig,
|
|
324
|
-
};
|
|
220
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
221
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
222
|
+
return cloned;
|
|
325
223
|
};
|
|
326
224
|
|
|
327
225
|
/** @param {string} workspacePath */
|
|
@@ -330,8 +228,8 @@ const readAuthConfig = (workspacePath) => {
|
|
|
330
228
|
const mergedConfig = getMergedWorkspaceConfig(configFile, workspacePath);
|
|
331
229
|
const parsed = AuthConfig.safeParse(mergedConfig);
|
|
332
230
|
if (!parsed.success) {
|
|
333
|
-
|
|
334
|
-
`
|
|
231
|
+
return new Error(
|
|
232
|
+
`Invalid auth config for ${workspacePath} (in config file ${CONFIG_PATH}). Have you run \`iterate setup\`?\n${z.prettifyError(parsed.error)}`,
|
|
335
233
|
);
|
|
336
234
|
}
|
|
337
235
|
return parsed.data;
|
|
@@ -350,11 +248,83 @@ const setCookiesToCookieHeader = (setCookies) => {
|
|
|
350
248
|
return [...byName.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
351
249
|
};
|
|
352
250
|
|
|
353
|
-
|
|
354
|
-
|
|
251
|
+
const impersonationUserIdCache = new Map();
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {{
|
|
255
|
+
* superadminAuthClient: any;
|
|
256
|
+
* userEmail: string;
|
|
257
|
+
* baseUrl: string;
|
|
258
|
+
* }} options
|
|
259
|
+
*/
|
|
260
|
+
const resolveImpersonationUserId = async ({ superadminAuthClient, userEmail, baseUrl }) => {
|
|
261
|
+
const normalizedEmail = userEmail.trim().toLowerCase();
|
|
262
|
+
const cacheKey = `${baseUrl}::${normalizedEmail}`;
|
|
263
|
+
const cachedUserId = impersonationUserIdCache.get(cacheKey);
|
|
264
|
+
if (cachedUserId) {
|
|
265
|
+
return cachedUserId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** @type {any[]} */
|
|
269
|
+
let users = [];
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const result = await superadminAuthClient.admin.listUsers({
|
|
273
|
+
query: {
|
|
274
|
+
filterField: "email",
|
|
275
|
+
filterOperator: "eq",
|
|
276
|
+
filterValue: normalizedEmail,
|
|
277
|
+
limit: 10,
|
|
278
|
+
},
|
|
279
|
+
fetchOptions: {
|
|
280
|
+
throw: true,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
users = Array.isArray(result?.users) ? result.users : [];
|
|
284
|
+
} catch {
|
|
285
|
+
const result = await superadminAuthClient.admin.listUsers({
|
|
286
|
+
query: {
|
|
287
|
+
searchField: "email",
|
|
288
|
+
searchOperator: "contains",
|
|
289
|
+
searchValue: normalizedEmail,
|
|
290
|
+
limit: 100,
|
|
291
|
+
},
|
|
292
|
+
fetchOptions: {
|
|
293
|
+
throw: true,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
users = Array.isArray(result?.users) ? result.users : [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const exactMatches = users.filter(
|
|
300
|
+
(user) =>
|
|
301
|
+
user &&
|
|
302
|
+
typeof user === "object" &&
|
|
303
|
+
"email" in user &&
|
|
304
|
+
typeof user.email === "string" &&
|
|
305
|
+
user.email.toLowerCase() === normalizedEmail &&
|
|
306
|
+
"id" in user &&
|
|
307
|
+
typeof user.id === "string",
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (exactMatches.length === 0) {
|
|
311
|
+
throw new Error(`No user found with email ${userEmail}`);
|
|
312
|
+
}
|
|
313
|
+
if (exactMatches.length > 1) {
|
|
314
|
+
throw new Error(`Multiple users found with email ${userEmail}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const resolvedUserId = exactMatches[0].id;
|
|
318
|
+
impersonationUserIdCache.set(cacheKey, resolvedUserId);
|
|
319
|
+
return resolvedUserId;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/** @param {import('zod').infer<typeof AuthConfig>} authConfig */
|
|
323
|
+
const osAuthDance = async (authConfig) => {
|
|
324
|
+
/** @type {string[] | undefined} */
|
|
355
325
|
let superadminSetCookie;
|
|
356
326
|
const authClient = createAuthClient({
|
|
357
|
-
baseURL: authConfig.
|
|
327
|
+
baseURL: authConfig.osBaseUrl,
|
|
358
328
|
fetchOptions: {
|
|
359
329
|
throw: true,
|
|
360
330
|
},
|
|
@@ -376,20 +346,26 @@ const authDance = async (authConfig) => {
|
|
|
376
346
|
});
|
|
377
347
|
|
|
378
348
|
const superadminAuthClient = createAuthClient({
|
|
379
|
-
baseURL: authConfig.
|
|
349
|
+
baseURL: authConfig.osBaseUrl,
|
|
380
350
|
fetchOptions: {
|
|
381
351
|
throw: true,
|
|
382
352
|
onRequest: (ctx) => {
|
|
383
|
-
ctx.headers.set("origin", authConfig.
|
|
353
|
+
ctx.headers.set("origin", authConfig.osBaseUrl);
|
|
384
354
|
ctx.headers.set("cookie", setCookiesToCookieHeader(superadminSetCookie));
|
|
385
355
|
},
|
|
386
356
|
},
|
|
387
357
|
plugins: [adminClient()],
|
|
388
358
|
});
|
|
389
359
|
|
|
360
|
+
const userId = await resolveImpersonationUserId({
|
|
361
|
+
superadminAuthClient,
|
|
362
|
+
userEmail: authConfig.userEmail,
|
|
363
|
+
baseUrl: authConfig.osBaseUrl,
|
|
364
|
+
});
|
|
365
|
+
|
|
390
366
|
let impersonateSetCookie;
|
|
391
367
|
await superadminAuthClient.admin.impersonateUser({
|
|
392
|
-
userId
|
|
368
|
+
userId,
|
|
393
369
|
fetchOptions: {
|
|
394
370
|
throw: true,
|
|
395
371
|
onResponse: (ctx) => {
|
|
@@ -401,11 +377,11 @@ const authDance = async (authConfig) => {
|
|
|
401
377
|
const userCookies = setCookiesToCookieHeader(impersonateSetCookie);
|
|
402
378
|
|
|
403
379
|
const userClient = createAuthClient({
|
|
404
|
-
baseURL: authConfig.
|
|
380
|
+
baseURL: authConfig.osBaseUrl,
|
|
405
381
|
fetchOptions: {
|
|
406
382
|
throw: true,
|
|
407
383
|
onRequest: (ctx) => {
|
|
408
|
-
ctx.headers.set("origin", authConfig.
|
|
384
|
+
ctx.headers.set("origin", authConfig.osBaseUrl);
|
|
409
385
|
ctx.headers.set("cookie", userCookies);
|
|
410
386
|
},
|
|
411
387
|
},
|
|
@@ -414,139 +390,38 @@ const authDance = async (authConfig) => {
|
|
|
414
390
|
return { userCookies, userClient };
|
|
415
391
|
};
|
|
416
392
|
|
|
417
|
-
/** @param {string}
|
|
418
|
-
const loadAppRouter = async (
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
393
|
+
/** @param {{baseUrl: string}} params */
|
|
394
|
+
const loadAppRouter = async (params) => {
|
|
395
|
+
const url = `${params.baseUrl}/api/trpc-cli-procedures`;
|
|
396
|
+
const response = await fetch(url);
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(`${url} got ${response.status}: ${await response.text()}`);
|
|
422
399
|
}
|
|
423
|
-
const rootModule = await import(pathToFileURL(appRouterPath).href);
|
|
424
|
-
if (!rootModule || typeof rootModule !== "object" || !("appRouter" in rootModule)) {
|
|
425
|
-
throw new Error(`Failed to load appRouter from ${appRouterPath}`);
|
|
426
|
-
}
|
|
427
|
-
return rootModule.appRouter;
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
/** @param {unknown} error */
|
|
431
|
-
const commandMissing = (error) => {
|
|
432
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
/** @param {SpawnOptions} options */
|
|
436
|
-
const run = ({ command, args, cwd, env }) => {
|
|
437
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
438
|
-
const child = spawn(command, args, {
|
|
439
|
-
cwd,
|
|
440
|
-
env,
|
|
441
|
-
stdio: "inherit",
|
|
442
|
-
});
|
|
443
400
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
child.on("close", (code, signal) => {
|
|
449
|
-
if (signal) {
|
|
450
|
-
rejectPromise(new Error(`${command} exited with signal ${signal}`));
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
resolvePromise(code ?? 0);
|
|
454
|
-
});
|
|
401
|
+
const router = await response.json().catch((e) => {
|
|
402
|
+
throw new Error(`${url} returned invalid router: ${e.message}`);
|
|
455
403
|
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
/** @param {SpawnOptions} options */
|
|
459
|
-
const runChecked = async ({ command, args, cwd, env }) => {
|
|
460
|
-
const code = await run({ command, args, cwd, env });
|
|
461
|
-
if (code !== 0) {
|
|
462
|
-
throw new Error(`Command failed (${code}): ${command} ${args.join(" ")}`);
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
/** @param {CheckoutOptions} options */
|
|
467
|
-
const ensureRepoCheckout = async ({ repoDir, repoRef, repoUrl }) => {
|
|
468
|
-
if (existsSync(repoDir)) {
|
|
469
|
-
if (!existsSync(join(repoDir, APP_ROUTER_PATH))) {
|
|
470
|
-
throw new Error(`Expected ${APP_ROUTER_PATH} in ${repoDir}.`);
|
|
471
|
-
}
|
|
472
|
-
if (!existsSync(join(repoDir, ".git"))) {
|
|
473
|
-
throw new Error(`Expected git checkout at ${repoDir}, but .git is missing.`);
|
|
474
|
-
}
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
mkdirSync(dirname(repoDir), { recursive: true });
|
|
479
|
-
const cloneArgs = ["clone", "--depth", "1"];
|
|
480
|
-
if (repoRef) {
|
|
481
|
-
cloneArgs.push("--branch", repoRef, "--single-branch");
|
|
482
|
-
}
|
|
483
|
-
cloneArgs.push(repoUrl, repoDir);
|
|
484
|
-
|
|
485
|
-
log(`cloning iterate repo into ${repoDir}`);
|
|
486
|
-
try {
|
|
487
|
-
await runChecked({ command: "git", args: cloneArgs });
|
|
488
|
-
const envClientPath = join(repoDir, "apps/os/env-client.ts");
|
|
489
|
-
// todo: remove this as soon as this branch is merged into main
|
|
490
|
-
writeFileSync(
|
|
491
|
-
envClientPath,
|
|
492
|
-
readFileSync(envClientPath, "utf8").replace("import.meta.env.", "import.meta.env?."),
|
|
493
|
-
);
|
|
494
|
-
} catch (error) {
|
|
495
|
-
if (commandMissing(error)) {
|
|
496
|
-
throw new Error("git is required but was not found on PATH.");
|
|
497
|
-
}
|
|
498
|
-
throw error;
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
/** @param {string} repoDir */
|
|
503
|
-
const hasInstalledDependencies = (repoDir) => {
|
|
504
|
-
return existsSync(join(repoDir, "node_modules", ".modules.yaml"));
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
/** @param {{ repoDir: string }} options */
|
|
508
|
-
const installDependencies = async ({ repoDir }) => {
|
|
509
|
-
log("installing dependencies with pnpm");
|
|
510
|
-
const installArgs = ["install", "--frozen-lockfile"];
|
|
511
|
-
try {
|
|
512
|
-
await runChecked({
|
|
513
|
-
command: "corepack",
|
|
514
|
-
args: ["pnpm", ...installArgs],
|
|
515
|
-
cwd: repoDir,
|
|
516
|
-
});
|
|
517
|
-
return;
|
|
518
|
-
} catch (error) {
|
|
519
|
-
if (!commandMissing(error)) {
|
|
520
|
-
throw error;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
try {
|
|
525
|
-
await runChecked({
|
|
526
|
-
command: "pnpm",
|
|
527
|
-
args: installArgs,
|
|
528
|
-
cwd: repoDir,
|
|
529
|
-
});
|
|
530
|
-
} catch (error) {
|
|
531
|
-
if (commandMissing(error)) {
|
|
532
|
-
throw new Error("pnpm/corepack is required but was not found on PATH.");
|
|
533
|
-
}
|
|
534
|
-
throw error;
|
|
404
|
+
if (!Array.isArray(router?.procedures)) {
|
|
405
|
+
throw new Error(`${url} returned invalid router: ${JSON.stringify(router)}`);
|
|
535
406
|
}
|
|
407
|
+
/** @type {{procedures: any[]}} */
|
|
408
|
+
return router;
|
|
536
409
|
};
|
|
537
410
|
|
|
538
|
-
/** @param {string}
|
|
539
|
-
const
|
|
540
|
-
const appRouter = await loadAppRouter(
|
|
541
|
-
|
|
411
|
+
/** @param {{ baseUrl: string }} params */
|
|
412
|
+
const getOsProcedures = async (params) => {
|
|
413
|
+
const appRouter = await loadAppRouter(params);
|
|
414
|
+
/** @type {{}} */
|
|
415
|
+
const proxiedRouter = proxify(appRouter.procedures, async () => {
|
|
542
416
|
return createTRPCClient({
|
|
543
417
|
links: [
|
|
544
418
|
httpLink({
|
|
545
|
-
url: `${
|
|
419
|
+
url: `${params.baseUrl}/api/trpc/`,
|
|
546
420
|
transformer: superjson,
|
|
547
421
|
fetch: async (request, init) => {
|
|
548
422
|
const authConfig = readAuthConfig(process.cwd());
|
|
549
|
-
|
|
423
|
+
if (authConfig instanceof Error) throw authConfig;
|
|
424
|
+
const { userCookies } = await osAuthDance(authConfig);
|
|
550
425
|
const headers = new Headers(init?.headers);
|
|
551
426
|
headers.set("cookie", userCookies);
|
|
552
427
|
return fetch(request, { ...init, headers });
|
|
@@ -556,92 +431,176 @@ const getRuntimeProcedures = async (repoDir) => {
|
|
|
556
431
|
});
|
|
557
432
|
});
|
|
558
433
|
|
|
559
|
-
return
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
434
|
+
return proxiedRouter;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Creates a fetch wrapper that calls /api/trpc-stream/* instead of /api/trpc/*.
|
|
439
|
+
* The streaming endpoint returns SSE: log lines as `event: log` and the final
|
|
440
|
+
* tRPC response as `event: response`, which we reassemble into a normal Response.
|
|
441
|
+
* @param {string} daemonBaseUrl
|
|
442
|
+
* @returns {typeof globalThis.fetch}
|
|
443
|
+
*/
|
|
444
|
+
const streamingFetch = (daemonBaseUrl) => {
|
|
445
|
+
return async (/** @type {any} */ input, /** @type {any} */ init) => {
|
|
446
|
+
// Rewrite URL from /api/trpc/X to /api/trpc-stream/X
|
|
447
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
448
|
+
const rewritten = url.replace(
|
|
449
|
+
`${daemonBaseUrl}/api/trpc/`,
|
|
450
|
+
`${daemonBaseUrl}/api/trpc-stream/`,
|
|
451
|
+
);
|
|
452
|
+
const res = await fetch(rewritten, init);
|
|
453
|
+
if (rewritten === url) return res;
|
|
454
|
+
|
|
455
|
+
const contentType = res.headers.get("content-type") || "";
|
|
456
|
+
// If the daemon didn't respond with SSE, pass through as-is (non-streaming endpoint)
|
|
457
|
+
if (!contentType.includes("text/event-stream")) return res;
|
|
458
|
+
// Parse SSE stream: print log events to stderr, collect the final response
|
|
459
|
+
const reader = res.body?.getReader();
|
|
460
|
+
if (!reader) return res;
|
|
461
|
+
const decoder = new TextDecoder();
|
|
462
|
+
let buffer = "";
|
|
463
|
+
/** @type {string | null} */
|
|
464
|
+
let responseBody = null;
|
|
465
|
+
while (true) {
|
|
466
|
+
const { done, value } = await reader.read();
|
|
467
|
+
if (done) break;
|
|
468
|
+
buffer += decoder.decode(value, { stream: true });
|
|
469
|
+
// Process complete SSE messages (double newline delimited)
|
|
470
|
+
const parts = buffer.split("\n\n");
|
|
471
|
+
buffer = parts.pop() || "";
|
|
472
|
+
for (const part of parts) {
|
|
473
|
+
const lines = part.split("\n");
|
|
474
|
+
const event = lines[0].split(": ")[1];
|
|
475
|
+
const data = lines[1].split(": ").slice(1).join(": ");
|
|
476
|
+
if (event === "log") {
|
|
477
|
+
/** @type {{level: "debug" | "info" | "warn" | "error"; args: unknown[]}} */
|
|
478
|
+
const detail = JSON.parse(data);
|
|
479
|
+
console[detail.level](...detail.args);
|
|
480
|
+
} else if (event === "response") {
|
|
481
|
+
responseBody = data;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Reconstruct a normal Response from the final payload so tRPC client is happy
|
|
486
|
+
return new Response(responseBody, {
|
|
487
|
+
status: res.status,
|
|
488
|
+
headers: { "content-type": "application/json" },
|
|
489
|
+
});
|
|
566
490
|
};
|
|
567
491
|
};
|
|
568
492
|
|
|
493
|
+
/** @param {{ daemonBaseUrl: string }} params */
|
|
494
|
+
const getDaemonProcedures = async (params) => {
|
|
495
|
+
const daemonRouter = await loadAppRouter({ baseUrl: params.daemonBaseUrl });
|
|
496
|
+
const proxiedRouter = proxify(daemonRouter.procedures, async () => {
|
|
497
|
+
return createTRPCClient({
|
|
498
|
+
links: [
|
|
499
|
+
httpLink({
|
|
500
|
+
url: `${params.daemonBaseUrl}/api/trpc/`,
|
|
501
|
+
fetch: streamingFetch(params.daemonBaseUrl),
|
|
502
|
+
}),
|
|
503
|
+
],
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return proxiedRouter;
|
|
508
|
+
};
|
|
509
|
+
|
|
569
510
|
const launcherProcedures = {
|
|
570
511
|
doctor: t.procedure
|
|
571
512
|
.meta({ description: "Show launcher config and resolved runtime options" })
|
|
572
513
|
.mutation(async () => {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
cwdRepoDir: runtime.cwdRepoDir ?? null,
|
|
582
|
-
repoExists: existsSync(runtime.repoDir),
|
|
583
|
-
dependenciesInstalled: hasInstalledDependencies(runtime.repoDir),
|
|
584
|
-
};
|
|
514
|
+
const configFile = readConfigFile();
|
|
515
|
+
const parsed = ConfigFile.safeParse(configFile);
|
|
516
|
+
if (!parsed.success) {
|
|
517
|
+
throw new Error(`Invalid config file ${CONFIG_PATH}: ${z.prettifyError(parsed.error)}`);
|
|
518
|
+
}
|
|
519
|
+
const current = readAuthConfig(process.cwd());
|
|
520
|
+
if (current instanceof Error) throw current;
|
|
521
|
+
return { configPath: CONFIG_PATH, current };
|
|
585
522
|
}),
|
|
586
523
|
setup: t.procedure
|
|
587
|
-
.input(SetupInput)
|
|
588
|
-
.meta({ description: "Configure auth + launcher defaults for current workspace" })
|
|
524
|
+
.input(SetupInput.partial())
|
|
525
|
+
.meta({ prompt: true, description: "Configure auth + launcher defaults for current workspace" })
|
|
589
526
|
.mutation(async ({ input }) => {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
repoPath = DEFAULT_REPO_DIR;
|
|
596
|
-
} else if (rawRepoPath === "local") {
|
|
597
|
-
if (!runtime.cwdRepoDir) {
|
|
598
|
-
throw new Error(
|
|
599
|
-
"'local' repoPath was selected but current directory is not inside an iterate repo",
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
repoPath = runtime.cwdRepoDir;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const next = writeLauncherConfig({
|
|
606
|
-
launcherPatch: { repoPath, autoInstall: input.autoInstall },
|
|
607
|
-
workspacePatch: {
|
|
608
|
-
baseUrl: input.baseUrl,
|
|
527
|
+
writeNewConfig({
|
|
528
|
+
scope: input.scope || "workspace",
|
|
529
|
+
patch: {
|
|
530
|
+
osBaseUrl: input.osBaseUrl,
|
|
531
|
+
daemonBaseUrl: input.daemonBaseUrl,
|
|
609
532
|
adminPasswordEnvVarName: input.adminPasswordEnvVarName,
|
|
610
|
-
|
|
533
|
+
userEmail: input.userEmail,
|
|
611
534
|
},
|
|
612
|
-
scope: input.scope,
|
|
613
535
|
workspacePath: process.cwd(),
|
|
614
536
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
};
|
|
620
|
-
}),
|
|
621
|
-
install: t.procedure
|
|
622
|
-
.meta({ description: "Clone repo if needed, then run pnpm install" })
|
|
623
|
-
.mutation(async () => {
|
|
624
|
-
const runtime = resolveRuntimeOptions();
|
|
625
|
-
await ensureRepoCheckout(runtime);
|
|
626
|
-
await installDependencies({ repoDir: runtime.repoDir });
|
|
627
|
-
return { repoDir: runtime.repoDir };
|
|
537
|
+
|
|
538
|
+
const current = readAuthConfig(process.cwd());
|
|
539
|
+
if (current instanceof Error) throw current;
|
|
540
|
+
return { configPath: CONFIG_PATH, current };
|
|
628
541
|
}),
|
|
542
|
+
|
|
543
|
+
whoami: t.procedure.mutation(async () => {
|
|
544
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
545
|
+
if (authConfig instanceof Error) throw authConfig;
|
|
546
|
+
const { userClient } = await osAuthDance(authConfig);
|
|
547
|
+
return await userClient.getSession();
|
|
548
|
+
}),
|
|
629
549
|
};
|
|
630
550
|
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
const runtime = resolveRuntimeOptions();
|
|
634
|
-
await ensureRepoCheckout(runtime);
|
|
551
|
+
const runCli = async () => {
|
|
552
|
+
const authConfig = readAuthConfig(process.cwd());
|
|
635
553
|
|
|
636
|
-
|
|
637
|
-
|
|
554
|
+
/** @type {(problem: string) => (e: Error) => {}} */
|
|
555
|
+
const errorProcedure = (problem) => (e) => {
|
|
556
|
+
const message = `${problem}: ${e.message}`;
|
|
557
|
+
return t.procedure.meta({ description: message }).mutation(() => {
|
|
558
|
+
throw new Error(problem, { cause: e });
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
/** @type {import("@trpc/server").AnyRouter[]} */
|
|
563
|
+
const routers = [t.router(launcherProcedures)];
|
|
564
|
+
|
|
565
|
+
if (authConfig instanceof Error) {
|
|
566
|
+
routers.push(
|
|
567
|
+
t.router({
|
|
568
|
+
os: errorProcedure(`Invalid auth config`)(authConfig),
|
|
569
|
+
daemon: errorProcedure(`Invalid auth config`)(authConfig),
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
} else {
|
|
573
|
+
const [osProcedures, daemonProcedures] = await Promise.allSettled([
|
|
574
|
+
getOsProcedures({ baseUrl: authConfig.osBaseUrl }),
|
|
575
|
+
getDaemonProcedures({ daemonBaseUrl: authConfig.daemonBaseUrl }),
|
|
576
|
+
]);
|
|
577
|
+
|
|
578
|
+
if (osProcedures.status === "fulfilled") {
|
|
579
|
+
routers.push(t.router({ os: osProcedures.value }));
|
|
580
|
+
} else {
|
|
581
|
+
routers.push(
|
|
582
|
+
t.router({
|
|
583
|
+
os: errorProcedure(`Couldn't connect to os at ${authConfig.osBaseUrl}`)(
|
|
584
|
+
osProcedures.reason,
|
|
585
|
+
),
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (daemonProcedures.status === "fulfilled") {
|
|
590
|
+
// don't nest daemon procedures under "daemon"
|
|
591
|
+
routers.push(daemonProcedures.value);
|
|
592
|
+
} else {
|
|
593
|
+
routers.push(
|
|
594
|
+
t.router({
|
|
595
|
+
daemon: errorProcedure(`Couldn't connect to daemon at ${authConfig.daemonBaseUrl}`)(
|
|
596
|
+
daemonProcedures.reason,
|
|
597
|
+
),
|
|
598
|
+
}),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
638
601
|
}
|
|
639
602
|
|
|
640
|
-
const
|
|
641
|
-
const router = t.router({
|
|
642
|
-
...launcherProcedures,
|
|
643
|
-
...runtimeProcedures,
|
|
644
|
-
});
|
|
603
|
+
const router = t.mergeRouters(...routers);
|
|
645
604
|
|
|
646
605
|
const cli = createCli({
|
|
647
606
|
router,
|
|
@@ -650,15 +609,13 @@ const runCli = async (args) => {
|
|
|
650
609
|
description: "Iterate CLI",
|
|
651
610
|
});
|
|
652
611
|
|
|
653
|
-
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
654
612
|
await cli.run({
|
|
655
613
|
prompts: isAgent ? undefined : prompts,
|
|
656
614
|
});
|
|
657
615
|
};
|
|
658
616
|
|
|
659
617
|
const main = async () => {
|
|
660
|
-
|
|
661
|
-
await runCli(args);
|
|
618
|
+
await runCli();
|
|
662
619
|
};
|
|
663
620
|
|
|
664
621
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "CLI for iterate",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@trpc/server": "^11.7.2",
|
|
41
41
|
"better-auth": "1.4.3",
|
|
42
42
|
"superjson": "^2.2.2",
|
|
43
|
-
"trpc-cli": "
|
|
43
|
+
"trpc-cli": "0.12.4",
|
|
44
44
|
"zod": "4.1.12"
|
|
45
45
|
}
|
|
46
46
|
}
|