iterate 0.1.1 → 0.2.2

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 ADDED
@@ -0,0 +1,134 @@
1
+ # iterate
2
+
3
+ ⚠️⚠️⚠️ Coming soon! `npx iterate` is a work-in-progress CLI for managing [iterate.com](https://iterate.com) agents ⚠️⚠️⚠️
4
+
5
+ CLI for Iterate.
6
+
7
+ Runs as a thin bootstrapper that:
8
+
9
+ 1. Resolves an `iterate/iterate` checkout.
10
+ 2. Clones/install deps when needed.
11
+ 3. Loads `apps/os/backend/trpc/root.ts` from that checkout.
12
+ 4. Exposes commands like `iterate os ...` and `iterate whoami`.
13
+
14
+ ## Requirements
15
+
16
+ - Node `>=22`
17
+ - `git`
18
+ - `pnpm` or `corepack`
19
+
20
+ ## Quick start
21
+
22
+ Run without installing globally:
23
+
24
+ ```bash
25
+ npx iterate --help
26
+ ```
27
+
28
+ Initial setup (writes auth + launcher config):
29
+
30
+ ```bash
31
+ npx iterate setup \
32
+ --base-url https://dev-yourname-os.dev.iterate.com \
33
+ --admin-password-env-var-name SERVICE_AUTH_TOKEN \
34
+ --user-email dev-yourname@iterate.com \
35
+ --repo-path managed \
36
+ --auto-install true \
37
+ --scope global
38
+ ```
39
+
40
+ Then run commands:
41
+
42
+ ```bash
43
+ npx iterate whoami
44
+ npx iterate os project list
45
+ ```
46
+
47
+ ## Commands
48
+
49
+ - `iterate setup` - configure auth + launcher defaults
50
+ - `iterate doctor` - print resolved config/runtime info
51
+ - `iterate install` - force clone/install for resolved checkout
52
+ - `iterate whoami`
53
+ - `iterate os ...`
54
+
55
+ ## Config file
56
+
57
+ Config path:
58
+
59
+ `${XDG_CONFIG_HOME:-~/.config}/iterate/config.json`
60
+
61
+ Config shape:
62
+
63
+ ```json
64
+ {
65
+ "global": {
66
+ "repoPath": "~/.local/share/iterate/repo",
67
+ "repoRef": "main",
68
+ "repoUrl": "https://github.com/iterate/iterate.git",
69
+ "autoInstall": true
70
+ },
71
+ "workspaces": {
72
+ "/absolute/workspace/path": {
73
+ "baseUrl": "https://dev-yourname-os.dev.iterate.com",
74
+ "adminPasswordEnvVarName": "SERVICE_AUTH_TOKEN",
75
+ "userEmail": "dev-yourname@iterate.com",
76
+ "repoPath": "/absolute/path/to/iterate",
77
+ "autoInstall": false
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ Merge precedence is shallow:
84
+
85
+ `global` -> `workspaces[process.cwd()]`
86
+
87
+ ## Repo checkout resolution
88
+
89
+ `repoPath` resolution order:
90
+
91
+ 1. `ITERATE_REPO_DIR`
92
+ 2. `workspaces[process.cwd()].repoPath`
93
+ 3. `global.repoPath`
94
+ 4. nearest parent directory containing `.git`, `pnpm-workspace.yaml`, and `apps/os/backend/trpc/root.ts`
95
+ 5. default managed checkout path `${XDG_DATA_HOME:-~/.local/share}/iterate/repo`
96
+
97
+ `repoPath` shortcuts in `setup`:
98
+
99
+ - `local` - nearest local iterate checkout
100
+ - `managed` - default managed checkout path
101
+
102
+ Environment overrides:
103
+
104
+ - `ITERATE_REPO_DIR`
105
+ - `ITERATE_REPO_REF`
106
+ - `ITERATE_REPO_URL`
107
+ - `ITERATE_AUTO_INSTALL` (`1/true` or `0/false`)
108
+
109
+ ## Local iterate dev
110
+
111
+ If you run inside an `iterate/iterate` clone, the CLI auto-detects it. In that mode, default `autoInstall` is `false`.
112
+
113
+ You can pin explicitly:
114
+
115
+ ```bash
116
+ npx iterate setup \
117
+ --base-url https://dev-yourname-os.dev.iterate.com \
118
+ --admin-password-env-var-name SERVICE_AUTH_TOKEN \
119
+ --user-email dev-yourname@iterate.com \
120
+ --repo-path local \
121
+ --auto-install false \
122
+ --scope workspace
123
+ ```
124
+
125
+ ## Publishing (maintainers)
126
+
127
+ From repo root:
128
+
129
+ ```bash
130
+ pnpm --filter ./packages/iterate typecheck
131
+ pnpm eslint packages/iterate/bin/iterate.js
132
+ pnpm prettier --check packages/iterate
133
+ pnpm --filter ./packages/iterate publish --access public
134
+ ```
package/bin/iterate.js ADDED
@@ -0,0 +1,744 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, isAbsolute, join, resolve } from "node:path";
8
+ import process from "node:process";
9
+ import { pathToFileURL } from "node:url";
10
+ import * as prompts from "@clack/prompts";
11
+ import { createTRPCClient, httpLink } from "@trpc/client";
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");
34
+
35
+ /**
36
+ * @typedef {{
37
+ * repoPath?: string;
38
+ * repoRef?: string;
39
+ * repoUrl?: string;
40
+ * autoInstall?: boolean;
41
+ * }} LauncherConfig
42
+ */
43
+
44
+ /**
45
+ * @typedef {{
46
+ * global?: Record<string, unknown>;
47
+ * workspaces?: Record<string, Record<string, unknown>>;
48
+ * } & Record<string, unknown>} ConfigFile
49
+ */
50
+
51
+ /** @typedef {"env" | "config" | "cwd" | "default"} RepoDirSource */
52
+
53
+ /**
54
+ * @typedef {{
55
+ * repoDir: string;
56
+ * repoDirSource: RepoDirSource;
57
+ * repoRef?: string;
58
+ * repoUrl: string;
59
+ * autoInstall: boolean;
60
+ * cwdRepoDir?: string;
61
+ * launcherConfig: LauncherConfig;
62
+ * }} RuntimeOptions
63
+ */
64
+
65
+ /**
66
+ * @typedef {{
67
+ * command: string;
68
+ * args: string[];
69
+ * cwd?: string;
70
+ * env?: Record<string, string | undefined>;
71
+ * }} SpawnOptions
72
+ */
73
+
74
+ /**
75
+ * @typedef {{
76
+ * repoDir: string;
77
+ * repoRef?: string;
78
+ * repoUrl: string;
79
+ * }} CheckoutOptions
80
+ */
81
+
82
+ const isAgent =
83
+ process.env.AGENT === "1" ||
84
+ process.env.OPENCODE === "1" ||
85
+ Boolean(process.env.OPENCODE_SESSION) ||
86
+ Boolean(process.env.CLAUDE_CODE);
87
+
88
+ const t = initTRPC.meta().create();
89
+
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
+ userEmail: z.string().describe("User email 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
+ userEmail: z.string(),
105
+ });
106
+
107
+ /** @param {string} message */
108
+ const log = (message) => {
109
+ process.stderr.write(`[iterate] ${message}\n`);
110
+ };
111
+
112
+ /**
113
+ * @param {unknown} value
114
+ * @returns {value is Record<string, unknown>}
115
+ */
116
+ const isObject = (value) => {
117
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
118
+ };
119
+
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
+ /** @returns {ConfigFile} */
159
+ const readConfigFile = () => {
160
+ if (!existsSync(CONFIG_PATH)) {
161
+ return {};
162
+ }
163
+ const rawText = readFileSync(CONFIG_PATH, "utf8");
164
+ let parsed;
165
+ try {
166
+ parsed = JSON.parse(rawText);
167
+ } catch (error) {
168
+ const detail = error instanceof Error ? error.message : String(error);
169
+ throw new Error(`Invalid JSON in ${CONFIG_PATH}: ${detail}`);
170
+ }
171
+
172
+ if (!isObject(parsed)) {
173
+ throw new Error(`${CONFIG_PATH} must contain a JSON object.`);
174
+ }
175
+ return parsed;
176
+ };
177
+
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
+ /**
197
+ * @param {ConfigFile} configFile
198
+ * @param {string} workspacePath
199
+ */
200
+ const getWorkspaceConfig = (configFile, workspacePath) => {
201
+ const workspaces = isObject(configFile.workspaces) ? configFile.workspaces : {};
202
+ const rawWorkspaceConfig = workspaces[workspacePath];
203
+ return isObject(rawWorkspaceConfig) ? rawWorkspaceConfig : {};
204
+ };
205
+
206
+ /**
207
+ * @param {ConfigFile} configFile
208
+ * @param {string} workspacePath
209
+ */
210
+ const getMergedWorkspaceConfig = (configFile, workspacePath) => {
211
+ return {
212
+ ...getGlobalConfig(configFile),
213
+ ...getWorkspaceConfig(configFile, workspacePath),
214
+ };
215
+ };
216
+
217
+ /** @param {string} workspacePath */
218
+ const readLauncherConfig = (workspacePath) => {
219
+ const configFile = readConfigFile();
220
+ return sanitizeLauncherConfig(getMergedWorkspaceConfig(configFile, workspacePath));
221
+ };
222
+
223
+ /**
224
+ * @param {{
225
+ * launcherPatch: Partial<LauncherConfig>;
226
+ * workspacePatch?: Record<string, unknown>;
227
+ * scope: "workspace" | "global";
228
+ * workspacePath: string;
229
+ * }} options
230
+ */
231
+ const writeLauncherConfig = ({ launcherPatch, workspacePatch, scope, workspacePath }) => {
232
+ const configFile = readConfigFile();
233
+ const existingGlobal = getGlobalConfig(configFile);
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))
266
+ );
267
+ };
268
+
269
+ /** @param {string} startDir */
270
+ const findNearestIterateRepo = (startDir) => {
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;
281
+ }
282
+ };
283
+
284
+ /** @returns {RuntimeOptions} */
285
+ const resolveRuntimeOptions = () => {
286
+ const launcherConfig = readLauncherConfig(process.cwd());
287
+ const cwdRepoDir = findNearestIterateRepo(process.cwd());
288
+ const envRepoDir = nonEmptyString(process.env.ITERATE_REPO_DIR);
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";
306
+ }
307
+
308
+ const repoRef = nonEmptyString(process.env.ITERATE_REPO_REF) ?? launcherConfig.repoRef;
309
+ const repoUrl =
310
+ nonEmptyString(process.env.ITERATE_REPO_URL) ?? launcherConfig.repoUrl ?? DEFAULT_REPO_URL;
311
+ const autoInstall =
312
+ parseBoolean(process.env.ITERATE_AUTO_INSTALL) ??
313
+ launcherConfig.autoInstall ??
314
+ (repoDirSource === "cwd" ? false : true);
315
+
316
+ return {
317
+ repoDir,
318
+ repoDirSource,
319
+ repoRef,
320
+ repoUrl,
321
+ autoInstall,
322
+ cwdRepoDir,
323
+ launcherConfig,
324
+ };
325
+ };
326
+
327
+ /** @param {string} workspacePath */
328
+ const readAuthConfig = (workspacePath) => {
329
+ const configFile = readConfigFile();
330
+ const mergedConfig = getMergedWorkspaceConfig(configFile, workspacePath);
331
+ const parsed = AuthConfig.safeParse(mergedConfig);
332
+ if (!parsed.success) {
333
+ throw new Error(
334
+ `Config file ${CONFIG_PATH} is missing auth config for ${workspacePath}. Have you run \`iterate setup\`?\n${z.prettifyError(parsed.error)}`,
335
+ );
336
+ }
337
+ return parsed.data;
338
+ };
339
+
340
+ /** @param {string[] | undefined} setCookies */
341
+ const setCookiesToCookieHeader = (setCookies) => {
342
+ const byName = new Map();
343
+ for (const c of setCookies ?? []) {
344
+ const pair = c.split(";")[0]?.trim();
345
+ if (!pair) continue;
346
+ const eq = pair.indexOf("=");
347
+ if (eq === -1) continue;
348
+ byName.set(pair.slice(0, eq), pair.slice(eq + 1));
349
+ }
350
+ return [...byName.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
351
+ };
352
+
353
+ const impersonationUserIdCache = new Map();
354
+
355
+ /**
356
+ * @param {{
357
+ * superadminAuthClient: any;
358
+ * userEmail: string;
359
+ * baseUrl: string;
360
+ * }} options
361
+ */
362
+ const resolveImpersonationUserId = async ({ superadminAuthClient, userEmail, baseUrl }) => {
363
+ const normalizedEmail = userEmail.trim().toLowerCase();
364
+ const cacheKey = `${baseUrl}::${normalizedEmail}`;
365
+ const cachedUserId = impersonationUserIdCache.get(cacheKey);
366
+ if (cachedUserId) {
367
+ return cachedUserId;
368
+ }
369
+
370
+ /** @type {any[]} */
371
+ let users = [];
372
+
373
+ try {
374
+ const result = await superadminAuthClient.admin.listUsers({
375
+ query: {
376
+ filterField: "email",
377
+ filterOperator: "eq",
378
+ filterValue: normalizedEmail,
379
+ limit: 10,
380
+ },
381
+ fetchOptions: {
382
+ throw: true,
383
+ },
384
+ });
385
+ users = Array.isArray(result?.users) ? result.users : [];
386
+ } catch {
387
+ const result = await superadminAuthClient.admin.listUsers({
388
+ query: {
389
+ searchField: "email",
390
+ searchOperator: "contains",
391
+ searchValue: normalizedEmail,
392
+ limit: 100,
393
+ },
394
+ fetchOptions: {
395
+ throw: true,
396
+ },
397
+ });
398
+ users = Array.isArray(result?.users) ? result.users : [];
399
+ }
400
+
401
+ const exactMatches = users.filter(
402
+ (user) =>
403
+ user &&
404
+ typeof user === "object" &&
405
+ "email" in user &&
406
+ typeof user.email === "string" &&
407
+ user.email.toLowerCase() === normalizedEmail &&
408
+ "id" in user &&
409
+ typeof user.id === "string",
410
+ );
411
+
412
+ if (exactMatches.length === 0) {
413
+ throw new Error(`No user found with email ${userEmail}`);
414
+ }
415
+ if (exactMatches.length > 1) {
416
+ throw new Error(`Multiple users found with email ${userEmail}`);
417
+ }
418
+
419
+ const resolvedUserId = exactMatches[0].id;
420
+ impersonationUserIdCache.set(cacheKey, resolvedUserId);
421
+ return resolvedUserId;
422
+ };
423
+
424
+ /** @param {z.infer<typeof AuthConfig>} authConfig */
425
+ const authDance = async (authConfig) => {
426
+ let superadminSetCookie;
427
+ const authClient = createAuthClient({
428
+ baseURL: authConfig.baseUrl,
429
+ fetchOptions: {
430
+ throw: true,
431
+ },
432
+ });
433
+ const password = process.env[authConfig.adminPasswordEnvVarName];
434
+ if (!password) {
435
+ throw new Error(`Password not found in env var ${authConfig.adminPasswordEnvVarName}`);
436
+ }
437
+
438
+ await authClient.signIn.email({
439
+ email: "superadmin@nustom.com",
440
+ password,
441
+ fetchOptions: {
442
+ throw: true,
443
+ onResponse: (ctx) => {
444
+ superadminSetCookie = ctx.response.headers.getSetCookie();
445
+ },
446
+ },
447
+ });
448
+
449
+ const superadminAuthClient = createAuthClient({
450
+ baseURL: authConfig.baseUrl,
451
+ fetchOptions: {
452
+ throw: true,
453
+ onRequest: (ctx) => {
454
+ ctx.headers.set("origin", authConfig.baseUrl);
455
+ ctx.headers.set("cookie", setCookiesToCookieHeader(superadminSetCookie));
456
+ },
457
+ },
458
+ plugins: [adminClient()],
459
+ });
460
+
461
+ const userId = await resolveImpersonationUserId({
462
+ superadminAuthClient,
463
+ userEmail: authConfig.userEmail,
464
+ baseUrl: authConfig.baseUrl,
465
+ });
466
+
467
+ let impersonateSetCookie;
468
+ await superadminAuthClient.admin.impersonateUser({
469
+ userId,
470
+ fetchOptions: {
471
+ throw: true,
472
+ onResponse: (ctx) => {
473
+ impersonateSetCookie = ctx.response.headers.getSetCookie();
474
+ },
475
+ },
476
+ });
477
+
478
+ const userCookies = setCookiesToCookieHeader(impersonateSetCookie);
479
+
480
+ const userClient = createAuthClient({
481
+ baseURL: authConfig.baseUrl,
482
+ fetchOptions: {
483
+ throw: true,
484
+ onRequest: (ctx) => {
485
+ ctx.headers.set("origin", authConfig.baseUrl);
486
+ ctx.headers.set("cookie", userCookies);
487
+ },
488
+ },
489
+ });
490
+
491
+ return { userCookies, userClient };
492
+ };
493
+
494
+ /** @param {string} repoDir */
495
+ const loadAppRouter = async (repoDir) => {
496
+ const appRouterPath = join(repoDir, APP_ROUTER_PATH);
497
+ if (!existsSync(appRouterPath)) {
498
+ throw new Error(`Could not find ${APP_ROUTER_PATH} under ${repoDir}.`);
499
+ }
500
+ const rootModule = await import(pathToFileURL(appRouterPath).href);
501
+ if (!rootModule || typeof rootModule !== "object" || !("appRouter" in rootModule)) {
502
+ throw new Error(`Failed to load appRouter from ${appRouterPath}`);
503
+ }
504
+ return rootModule.appRouter;
505
+ };
506
+
507
+ /** @param {unknown} error */
508
+ const commandMissing = (error) => {
509
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
510
+ };
511
+
512
+ /** @param {SpawnOptions} options */
513
+ const run = ({ command, args, cwd, env }) => {
514
+ return new Promise((resolvePromise, rejectPromise) => {
515
+ const child = spawn(command, args, {
516
+ cwd,
517
+ env,
518
+ stdio: "inherit",
519
+ });
520
+
521
+ child.on("error", (error) => {
522
+ rejectPromise(error);
523
+ });
524
+
525
+ child.on("close", (code, signal) => {
526
+ if (signal) {
527
+ rejectPromise(new Error(`${command} exited with signal ${signal}`));
528
+ return;
529
+ }
530
+ resolvePromise(code ?? 0);
531
+ });
532
+ });
533
+ };
534
+
535
+ /** @param {SpawnOptions} options */
536
+ const runChecked = async ({ command, args, cwd, env }) => {
537
+ const code = await run({ command, args, cwd, env });
538
+ if (code !== 0) {
539
+ throw new Error(`Command failed (${code}): ${command} ${args.join(" ")}`);
540
+ }
541
+ };
542
+
543
+ /** @param {CheckoutOptions} options */
544
+ const ensureRepoCheckout = async ({ repoDir, repoRef, repoUrl }) => {
545
+ if (existsSync(repoDir)) {
546
+ if (!existsSync(join(repoDir, APP_ROUTER_PATH))) {
547
+ throw new Error(`Expected ${APP_ROUTER_PATH} in ${repoDir}.`);
548
+ }
549
+ if (!existsSync(join(repoDir, ".git"))) {
550
+ throw new Error(`Expected git checkout at ${repoDir}, but .git is missing.`);
551
+ }
552
+ return;
553
+ }
554
+
555
+ mkdirSync(dirname(repoDir), { recursive: true });
556
+ const cloneArgs = ["clone", "--depth", "1"];
557
+ if (repoRef) {
558
+ cloneArgs.push("--branch", repoRef, "--single-branch");
559
+ }
560
+ cloneArgs.push(repoUrl, repoDir);
561
+
562
+ log(`cloning iterate repo into ${repoDir}`);
563
+ try {
564
+ await runChecked({ command: "git", args: cloneArgs });
565
+ const envClientPath = join(repoDir, "apps/os/env-client.ts");
566
+ // todo: remove this as soon as this branch is merged into main
567
+ writeFileSync(
568
+ envClientPath,
569
+ readFileSync(envClientPath, "utf8").replace("import.meta.env.", "import.meta.env?."),
570
+ );
571
+ } catch (error) {
572
+ if (commandMissing(error)) {
573
+ throw new Error("git is required but was not found on PATH.");
574
+ }
575
+ throw error;
576
+ }
577
+ };
578
+
579
+ /** @param {string} repoDir */
580
+ const hasInstalledDependencies = (repoDir) => {
581
+ return existsSync(join(repoDir, "node_modules", ".modules.yaml"));
582
+ };
583
+
584
+ /** @param {{ repoDir: string }} options */
585
+ const installDependencies = async ({ repoDir }) => {
586
+ log("installing dependencies with pnpm");
587
+ const installArgs = ["install", "--frozen-lockfile"];
588
+ try {
589
+ await runChecked({
590
+ command: "corepack",
591
+ args: ["pnpm", ...installArgs],
592
+ cwd: repoDir,
593
+ });
594
+ return;
595
+ } catch (error) {
596
+ if (!commandMissing(error)) {
597
+ throw error;
598
+ }
599
+ }
600
+
601
+ try {
602
+ await runChecked({
603
+ command: "pnpm",
604
+ args: installArgs,
605
+ cwd: repoDir,
606
+ });
607
+ } catch (error) {
608
+ if (commandMissing(error)) {
609
+ throw new Error("pnpm/corepack is required but was not found on PATH.");
610
+ }
611
+ throw error;
612
+ }
613
+ };
614
+
615
+ /** @param {string} repoDir */
616
+ const getRuntimeProcedures = async (repoDir) => {
617
+ const appRouter = await loadAppRouter(repoDir);
618
+ const proxiedRouter = proxify(appRouter, async () => {
619
+ return createTRPCClient({
620
+ links: [
621
+ httpLink({
622
+ url: `${readAuthConfig(process.cwd()).baseUrl}/api/trpc/`,
623
+ transformer: superjson,
624
+ fetch: async (request, init) => {
625
+ const authConfig = readAuthConfig(process.cwd());
626
+ const { userCookies } = await authDance(authConfig);
627
+ const headers = new Headers(init?.headers);
628
+ headers.set("cookie", userCookies);
629
+ return fetch(request, { ...init, headers });
630
+ },
631
+ }),
632
+ ],
633
+ });
634
+ });
635
+
636
+ return {
637
+ whoami: t.procedure.mutation(async () => {
638
+ const authConfig = readAuthConfig(process.cwd());
639
+ const { userClient } = await authDance(authConfig);
640
+ return await userClient.getSession();
641
+ }),
642
+ os: proxiedRouter,
643
+ };
644
+ };
645
+
646
+ const launcherProcedures = {
647
+ doctor: t.procedure
648
+ .meta({ description: "Show launcher config and resolved runtime options" })
649
+ .mutation(async () => {
650
+ const runtime = resolveRuntimeOptions();
651
+ return {
652
+ configPath: CONFIG_PATH,
653
+ repoDir: runtime.repoDir,
654
+ repoDirSource: runtime.repoDirSource,
655
+ autoInstall: runtime.autoInstall,
656
+ repoRef: runtime.repoRef ?? null,
657
+ repoUrl: runtime.repoUrl,
658
+ cwdRepoDir: runtime.cwdRepoDir ?? null,
659
+ repoExists: existsSync(runtime.repoDir),
660
+ dependenciesInstalled: hasInstalledDependencies(runtime.repoDir),
661
+ };
662
+ }),
663
+ setup: t.procedure
664
+ .input(SetupInput)
665
+ .meta({ description: "Configure auth + launcher defaults for current workspace" })
666
+ .mutation(async ({ input }) => {
667
+ const runtime = resolveRuntimeOptions();
668
+
669
+ const rawRepoPath = input.repoPath.trim().toLowerCase();
670
+ let repoPath = normalizePath(input.repoPath);
671
+ if (rawRepoPath === "managed") {
672
+ repoPath = DEFAULT_REPO_DIR;
673
+ } else if (rawRepoPath === "local") {
674
+ if (!runtime.cwdRepoDir) {
675
+ throw new Error(
676
+ "'local' repoPath was selected but current directory is not inside an iterate repo",
677
+ );
678
+ }
679
+ repoPath = runtime.cwdRepoDir;
680
+ }
681
+
682
+ const next = writeLauncherConfig({
683
+ launcherPatch: { repoPath, autoInstall: input.autoInstall },
684
+ workspacePatch: {
685
+ baseUrl: input.baseUrl,
686
+ adminPasswordEnvVarName: input.adminPasswordEnvVarName,
687
+ userEmail: input.userEmail,
688
+ },
689
+ scope: input.scope,
690
+ workspacePath: process.cwd(),
691
+ });
692
+ return {
693
+ configPath: CONFIG_PATH,
694
+ launcher: sanitizeLauncherConfig(getMergedWorkspaceConfig(next, process.cwd())),
695
+ scope: input.scope,
696
+ };
697
+ }),
698
+ install: t.procedure
699
+ .meta({ description: "Clone repo if needed, then run pnpm install" })
700
+ .mutation(async () => {
701
+ const runtime = resolveRuntimeOptions();
702
+ await ensureRepoCheckout(runtime);
703
+ await installDependencies({ repoDir: runtime.repoDir });
704
+ return { repoDir: runtime.repoDir };
705
+ }),
706
+ };
707
+
708
+ /** @param {string[]} args */
709
+ const runCli = async (args) => {
710
+ const runtime = resolveRuntimeOptions();
711
+ await ensureRepoCheckout(runtime);
712
+
713
+ if (runtime.autoInstall && !hasInstalledDependencies(runtime.repoDir)) {
714
+ await installDependencies({ repoDir: runtime.repoDir });
715
+ }
716
+
717
+ const runtimeProcedures = await getRuntimeProcedures(runtime.repoDir);
718
+ const router = t.router({
719
+ ...launcherProcedures,
720
+ ...runtimeProcedures,
721
+ });
722
+
723
+ const cli = createCli({
724
+ router,
725
+ name: "iterate",
726
+ version: "0.0.1",
727
+ description: "Iterate CLI",
728
+ });
729
+
730
+ process.argv = [process.argv[0], process.argv[1], ...args];
731
+ await cli.run({
732
+ prompts: isAgent ? undefined : prompts,
733
+ });
734
+ };
735
+
736
+ const main = async () => {
737
+ const args = process.argv.slice(2);
738
+ await runCli(args);
739
+ };
740
+
741
+ main().catch((error) => {
742
+ console.error(error);
743
+ process.exit(1);
744
+ });
package/package.json CHANGED
@@ -1,28 +1,46 @@
1
1
  {
2
2
  "name": "iterate",
3
- "version": "0.1.1",
4
- "description": "some iterator functions",
5
- "main": "index.js",
6
- "directories": {
7
- "test": "test"
8
- },
9
- "devDependencies": {
10
- "it-is": "~1.0.2"
11
- },
12
- "scripts": {
13
- "test": "synct test/*.js"
14
- },
3
+ "version": "0.2.2",
4
+ "description": "CLI for iterate",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
15
7
  "repository": {
16
8
  "type": "git",
17
- "url": "git://github.com/dominictarr/iterate.git"
9
+ "url": "git+https://github.com/iterate/iterate.git",
10
+ "directory": "packages/iterate"
11
+ },
12
+ "homepage": "https://github.com/iterate/iterate#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/iterate/iterate/issues"
18
15
  },
19
16
  "keywords": [
20
- "iterator",
21
- "each",
22
- "sync",
23
- "map",
24
- "filter"
17
+ "iterate",
18
+ "cli",
19
+ "trpc"
25
20
  ],
26
- "author": "Dominic Tarr <dominic.tarr@gmail.com> (dominictarr.com)",
27
- "license": "MIT"
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "bin": {
28
+ "iterate": "./bin/iterate.js"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "typecheck": "node --check ./bin/iterate.js"
36
+ },
37
+ "dependencies": {
38
+ "@clack/prompts": "^1.0.0",
39
+ "@trpc/client": "^11.7.2",
40
+ "@trpc/server": "^11.7.2",
41
+ "better-auth": "1.4.3",
42
+ "superjson": "^2.2.2",
43
+ "trpc-cli": "^0.12.2",
44
+ "zod": "4.1.12"
45
+ }
28
46
  }
package/LICENSE DELETED
@@ -1,24 +0,0 @@
1
- The MIT License
2
-
3
- Copyright (c) 2013 Dominic Tarr
4
-
5
- Permission is hereby granted, free of charge,
6
- to any person obtaining a copy of this software and
7
- associated documentation files (the "Software"), to
8
- deal in the Software without restriction, including
9
- without limitation the rights to use, copy, modify,
10
- merge, publish, distribute, sublicense, and/or sell
11
- copies of the Software, and to permit persons to whom
12
- the Software is furnished to do so,
13
- subject to the following conditions:
14
-
15
- The above copyright notice and this permission notice
16
- shall be included in all copies or substantial portions of the Software.
17
-
18
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
22
- ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.markdown DELETED
@@ -1,14 +0,0 @@
1
- # iterator
2
-
3
- some iterator functions,
4
- * each
5
- * map
6
- * filter
7
- * times
8
- * map
9
- * find
10
- * mapKeys
11
- * mapToArray
12
-
13
- all work objects and on arrays
14
-
package/index.js DELETED
@@ -1,152 +0,0 @@
1
-
2
- //
3
- // adds all the fields from obj2 onto obj1
4
- //
5
-
6
- var each = exports.each = function (obj,iterator){
7
- var keys = Object.keys(obj)
8
- keys.forEach(function (key){
9
- iterator(obj[key],key,obj)
10
- })
11
- }
12
-
13
- var RX = /sadf/.constructor
14
- function rx (iterator ){
15
- return iterator instanceof RX ? function (str) {
16
- var m = iterator.exec(str)
17
- return m && (m[1] ? m[1] : m[0])
18
- } : iterator
19
- }
20
-
21
- var times = exports.times = function () {
22
- var args = [].slice.call(arguments)
23
- , iterator = rx(args.pop())
24
- , m = args.pop()
25
- , i = args.shift()
26
- , j = args.shift()
27
- , diff, dir
28
- , a = []
29
-
30
- i = 'number' === typeof i ? i : 1
31
- diff = j ? j - i : 1
32
- dir = i < m
33
- if(m == i)
34
- throw new Error('steps cannot be the same: '+m+', '+i)
35
- for (; dir ? i <= m : m <= i; i += diff)
36
- a.push(iterator(i))
37
- return a
38
- }
39
-
40
- var map = exports.map = function (obj, iterator){
41
- iterator = rx(iterator)
42
- if(Array.isArray(obj))
43
- return obj.map(iterator)
44
- if('number' === typeof obj)
45
- return times.apply(null, [].slice.call(arguments))
46
- //return if null ?
47
- var keys = Object.keys(obj)
48
- , r = {}
49
- keys.forEach(function (key){
50
- r[key] = iterator(obj[key],key,obj)
51
- })
52
- return r
53
- }
54
-
55
- var findReturn = exports.findReturn = function (obj, iterator) {
56
- iterator = rx(iterator)
57
- if(obj == null)
58
- return
59
- var keys = Object.keys(obj)
60
- , l = keys.length
61
- for (var i = 0; i < l; i ++) {
62
- var key = keys[i]
63
- , value = obj[key]
64
- var r = iterator(value, key)
65
- if(r) return r
66
- }
67
- }
68
-
69
- var find = exports.find = function (obj, iterator) {
70
- iterator = rx(iterator)
71
- return findReturn (obj, function (v, k) {
72
- var r = iterator(v, k)
73
- if(r) return v
74
- })
75
- }
76
-
77
- var findKey = exports.findKey = function (obj, iterator) {
78
- iterator = rx(iterator)
79
- return findReturn (obj, function (v, k) {
80
- var r = iterator(v, k)
81
- if(r) return k
82
- })
83
- }
84
-
85
- var filter = exports.filter = function (obj, iterator){
86
- iterator = rx (iterator)
87
-
88
- if(Array.isArray(obj))
89
- return obj.filter(iterator)
90
-
91
- var keys = Object.keys(obj)
92
- , r = {}
93
- keys.forEach(function (key){
94
- var v
95
- if(iterator(v = obj[key],key,obj))
96
- r[key] = v
97
- })
98
- return r
99
- }
100
-
101
- var mapKeys = exports.mapKeys = function (ary, iterator){
102
- var r = {}
103
- iterator = rx(iterator)
104
- each(ary, function (v,k){
105
- r[v] = iterator(v,k)
106
- })
107
- return r
108
- }
109
-
110
-
111
- var mapToArray = exports.mapToArray = function (ary, iterator){
112
- var r = []
113
- iterator = rx(iterator)
114
- each(ary, function (v,k){
115
- r.push(iterator(v,k))
116
- })
117
- return r
118
- }
119
-
120
- var path = exports.path = function (object, path) {
121
-
122
- for (var i in path) {
123
- if(object == null) return undefined
124
- var key = path[i]
125
- object = object[key]
126
- }
127
- return object
128
- }
129
-
130
- /*
131
- NOTE: naive implementation.
132
- `match` must not contain circular references.
133
- */
134
-
135
- var setPath = exports.setPath = function (object, path, value) {
136
-
137
- for (var i in path) {
138
- var key = path[i]
139
- if(object[key] == null) object[key] = (
140
- i + 1 == path.length ? value : {}
141
- )
142
- object = object[key]
143
- }
144
- }
145
-
146
- var join = exports.join = function (A, B, it) {
147
- each(A, function (a, ak) {
148
- each(B, function (b, bk) {
149
- it(a, b, ak, bk)
150
- })
151
- })
152
- }
@@ -1,188 +0,0 @@
1
- var objects = require('..')
2
- , it = require('it-is')
3
-
4
- exports ['each'] = function (){
5
- var on = {a: 1,b: 2, c: 3}, count = 0
6
- objects.each(on, function(v,k){
7
- it(v).equal(on[k])
8
- count ++
9
- })
10
- it(count).equal(3)
11
- }
12
- exports ['map'] = function (){
13
-
14
- var on = {a: 1,b: 2, c: 3}, count = 0
15
- var off =
16
- objects.map(on, function(v,k){
17
- it(v).equal(on[k])
18
- count ++
19
- return v * 2
20
- })
21
- it(count).equal(3)
22
- it(off).deepEqual({
23
- a: 2, b: 4,c: 6
24
- })
25
- }
26
-
27
- exports ['map -- {}'] = function (){
28
-
29
- var on = {}, count = 0
30
- var off =
31
- objects.map(on, function(v,k){
32
- count ++
33
- })
34
- it(count).equal(0)
35
- it(off).deepEqual({})
36
- }
37
-
38
- exports ['map -- {}'] = function (){
39
-
40
- var on = {}, count = 0
41
- var off =
42
- objects.map(on, function(v,k){
43
- count ++
44
- })
45
- it(count).equal(0)
46
- it(off).deepEqual({})
47
- }
48
-
49
- exports ['map on numbers -- start at 1'] = function () {
50
-
51
- var seven = objects.map(7, function (n) {
52
- return n
53
- })
54
-
55
- it(seven).deepEqual([1,2,3,4,5,6,7])
56
-
57
- }
58
-
59
- exports ['map on numbers: 0 - 8'] = function () {
60
-
61
- var seven = objects.map(0, 8, function (n) {
62
- return n
63
- })
64
-
65
- it(seven).deepEqual([0,1,2,3,4,5,6,7,8])
66
-
67
- }
68
-
69
- exports ['map even numbers: 2,4...12'] = function () {
70
-
71
- var seven = objects.map(2, 4, 12, function (n) {
72
- return n
73
- })
74
-
75
- it(seven).deepEqual([2,4,6,8,10,12])
76
- }
77
-
78
- exports ['map even numbers: 12, 10...2 in reverse'] = function () {
79
-
80
- var seven = objects.map(12, 10, 2, function (n) {
81
- return n
82
- })
83
-
84
- it(seven).deepEqual([2,4,6,8,10,12].reverse())
85
-
86
- }
87
-
88
-
89
- exports ['filter'] = function (){
90
-
91
- var on = {a: 1,b: 2, c: 3, d:4, e:5, f:6, g:7}, count = 0
92
-
93
- it(objects.filter(on, function (x){
94
- return !(x % 2)
95
- })).deepEqual({b: 2, d:4, f:6})
96
- }
97
-
98
- exports ['filter with regexp'] = function (){
99
-
100
- var on = [ '.git',
101
- 'test',
102
- 'objects.js',
103
- 'index.js',
104
- 'package.json',
105
- 'types.js',
106
- 'readme.markdown',
107
- '.gitignore',
108
- 'arrays.js' ], count = 0
109
-
110
- it(objects.filter(on, /^.*\.js$/))
111
- .deepEqual([
112
- 'objects.js',
113
- 'index.js',
114
- 'types.js',
115
- 'arrays.js' ])
116
- }
117
-
118
- exports ['mapKeys -- create a object from a list of keys and a function'] = function (){
119
-
120
- var keys = ['foo','bar','xux']
121
-
122
- it(objects.mapKeys(keys, function (k){
123
- return k.toUpperCase()
124
- })).deepEqual({foo: 'FOO', bar: 'BAR', xux: 'XUX'})
125
-
126
- }
127
-
128
- exports ['mapToArray'] = function (){
129
-
130
- var on = {a: 1,b: 2, c: 3, d:4, e:5, f:6, g:7}, count = 0
131
-
132
- it(objects.mapToArray(on,function (v){
133
- return v
134
- })).deepEqual([1,2,3,4,5,6,7])
135
-
136
- }
137
-
138
- exports ['find'] = function () {
139
-
140
- //
141
- // sometime need to stop before the end, find is like that
142
- //
143
-
144
- var numbers = [3, 5, 3, 7, 77, 21, 4]
145
-
146
- it(objects.find(numbers, function (e) { return !(e % 2)})).equal(4)
147
- it(objects.findKey(numbers, function (e) { return !(e % 2)})).equal(6)
148
- it(objects.findReturn(numbers, function (e) { return !(e % 2)})).equal(true)
149
-
150
- it(objects.find([], function () {})).equal(null)
151
- it(objects.find(null, function () {})).equal(null)
152
-
153
- }
154
-
155
- exports ['path'] = function () {
156
-
157
- var a1 = {A: 1}
158
-
159
- it(objects.path(a1, ['A'])).equal(1)
160
- it(objects.path({A: {B: 4}}, ['A', 'B'])).equal(4)
161
- it(objects.path({A: {B: 4}}, ['A', 'C'])).strictEqual(undefined)
162
- it(objects.path(null, ['A', 'C'])).strictEqual(undefined)
163
- it(objects.path({Z: [0, 1, 2]}, ['Z', 2])).strictEqual(2)
164
- it(objects.path({Z: [0, 1, 2]}, ['Z', 0])).strictEqual(0)
165
- it(objects.path({Z: [0, 1, 2]}, ['Z', 0, 'T'])).strictEqual(undefined)
166
- it(objects.path({Z: null}, ['Z', 0, 'T'])).strictEqual(undefined)
167
- it(objects.path({Z: null}, ['Z'])).strictEqual(null)
168
-
169
- }
170
-
171
- exports ['setPath'] = function () {
172
-
173
- var x = {}
174
- , c = Math.random()
175
- , z = Math.random()
176
- objects.setPath(x, ['a','b','c'], c)
177
- it.equal(x.a.b.c, c)
178
- objects.setPath(x, ['z'], z)
179
- it.equal(x.z, z)
180
- }
181
-
182
- exports['join'] = function () {
183
- var ary = []
184
- objects.join('abc'.split(''), 'ijk'.split(''), function (a, i) {
185
- ary.push([a, i])
186
- })
187
- it.equal(ary.length, 9)
188
- }