@trypattern/sdk 0.1.4 → 0.1.5

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/dist/cli/app.js CHANGED
@@ -13,6 +13,7 @@ import { homedir } from 'node:os';
13
13
  import { delimiter, dirname, extname, join, relative, resolve } from 'node:path';
14
14
  import { pathToFileURL } from 'node:url';
15
15
  import { createHash } from 'node:crypto';
16
+ import { z } from 'zod';
16
17
  import { resetLocalDatabase } from "../vite/localWorker.js";
17
18
  import { login, publish, pull, restore, versions as listVersions } from "./publishing.js";
18
19
  import { toolBin } from "./tools.js";
@@ -73,7 +74,7 @@ const CONTACT_SHEET = join(SCREENSHOTS_DIR, 'contact-sheet.png');
73
74
  const SCREENSHOT_WORKER = join(SDK_SOURCE, 'cli', `screenshot${SDK_EXTENSION}`);
74
75
  const VISUAL_URL_ENV = 'PATTERN_VISUAL_URL';
75
76
  const GLOBAL_FLAGS = ['--json', '--help', '-h'];
76
- const VALUE_FLAGS = new Set(['--commands', '--name', '--as']);
77
+ const VALUE_FLAGS = new Set(['--commands', '--name', '--as', '--code']);
77
78
  const SHIM_SOURCE = `#!/bin/sh
78
79
  ${SHIM_MARKER_LINE}
79
80
  dir=$PWD
@@ -266,8 +267,13 @@ and restore sign in the same way when they need to, so this is rarely run on its
266
267
  details: `app pull sundayparty
267
268
  Downloads the source of the site's latest live version into this folder when it is empty, or into
268
269
  ./<name> otherwise, writes pattern.json, installs, and runs app setup. Never needs git; when the
269
- Command Line Tools are installed it also makes a first commit. Signs in first if needed (see app login).`,
270
- run: (argument) => pull(publishingIo(), argument, process.cwd()),
270
+ Command Line Tools are installed it also makes a first commit. Signs in first if needed (see app login).
271
+ --code <setup code> signs in with the one-time setup code from onpattern.app instead of the browser.`,
272
+ run: (argument, options) => {
273
+ const code = options.get('--code');
274
+ return pull(publishingIo(), argument, process.cwd(), typeof code === 'string' ? code : undefined);
275
+ },
276
+ flags: ['--code'],
271
277
  takesArgument: true,
272
278
  runsAnywhere: true
273
279
  },
@@ -297,7 +303,7 @@ every row and column added since, which version 3's code ignores.`,
297
303
  },
298
304
  setup: {
299
305
  summary: 'Verify Node/pnpm major versions, pnpm install, install the global `app` shim',
300
- details: `Idempotent. Fails unless Node and pnpm have the major versions in .nvmrc and package.json "packageManager".
306
+ details: `Idempotent. Fails unless Node and pnpm have the major versions in .nvmrc and package.json devEngines.
301
307
  Then runs \`pnpm install\` and installs ${SHIM_PATH}, a tiny shim that walks up from the
302
308
  current directory to the nearest project launcher (an executable ./app containing the line
303
309
  ${LAUNCHER_MARKER_LINE}) and runs it, so each project's CLI lives in its repo and the shim
@@ -1217,13 +1223,16 @@ async function build() {
1217
1223
  function majorOf(version) {
1218
1224
  return version.split('.')[0] ?? '';
1219
1225
  }
1226
+ const packagePins = z.object({
1227
+ devEngines: z.object({
1228
+ packageManager: z.object({ version: z.string() })
1229
+ })
1230
+ });
1220
1231
  function readPins() {
1221
1232
  const node = readText(join(ROOT, '.nvmrc')).trim().replace(/^v/, '');
1222
- const pkg = JSON.parse(readText(join(ROOT, 'package.json')));
1223
- const pm = typeof pkg === 'object' && pkg !== null && 'packageManager' in pkg
1224
- ? String(pkg.packageManager)
1225
- : '';
1226
- return { node, pnpm: pm.replace(/^pnpm@/, '').split('+')[0] ?? '' };
1233
+ const pkg = packagePins.safeParse(JSON.parse(readText(join(ROOT, 'package.json'))));
1234
+ const pnpm = pkg.success ? pkg.data.devEngines.packageManager.version : '';
1235
+ return { node, pnpm: pnpm.replace(/^>=/, '') };
1227
1236
  }
1228
1237
  function isPatternShim(source) {
1229
1238
  const lines = source.split('\n');
@@ -1260,7 +1269,7 @@ async function setup() {
1260
1269
  problems.push(`Node ${haveNode} is not Node ${majorOf(pins.node)} (.nvmrc says ${pins.node})\n fix pnpm env use --global ${majorOf(pins.node)}`);
1261
1270
  }
1262
1271
  if (majorOf(havePnpm) !== majorOf(pins.pnpm)) {
1263
- problems.push(`pnpm ${havePnpm || '(not found)'} is not pnpm ${majorOf(pins.pnpm)} (package.json packageManager says ${pins.pnpm})\n fix curl -fsSL https://get.pnpm.io/install.sh | sh -`);
1272
+ problems.push(`pnpm ${havePnpm || '(not found)'} is not pnpm ${majorOf(pins.pnpm)} (package.json devEngines says ${pins.pnpm} or newer)\n fix curl -fsSL https://get.pnpm.io/install.sh | sh -`);
1264
1273
  }
1265
1274
  const versions = { node: haveNode, pnpm: havePnpm, expected: pins };
1266
1275
  if (problems.length > 0) {
@@ -1302,7 +1311,7 @@ async function setup() {
1302
1311
  function help() {
1303
1312
  const width = Math.max(...Object.keys(COMMANDS).map((n) => n.length));
1304
1313
  const lines = Object.entries(COMMANDS).map(([name, cmd]) => `app ${name.padEnd(width)} ${cmd.summary}`);
1305
- lines.push('Flags: --json (machine-readable stdout), <command> --help (details and its own flags: start --no-open, test --visual, screenshot --commands --name --as --signed-out)');
1314
+ lines.push('Flags: --json (machine-readable stdout), <command> --help (details and its own flags: start --no-open, test --visual, screenshot --commands --name --as --signed-out, pull --code)');
1306
1315
  return {
1307
1316
  code: 0,
1308
1317
  data: {
@@ -14,7 +14,7 @@ export type PublishingIo = {
14
14
  sdkVersion: string;
15
15
  };
16
16
  export declare function login(io: PublishingIo): Promise<Outcome>;
17
- export declare function pull(io: PublishingIo, name: string | undefined, cwd: string): Promise<Outcome>;
17
+ export declare function pull(io: PublishingIo, name: string | undefined, cwd: string, setupCode: string | undefined): Promise<Outcome>;
18
18
  export declare function publish(io: PublishingIo, root: string, isStarter: boolean): Promise<Outcome>;
19
19
  export declare function versions(io: PublishingIo, root: string): Promise<Outcome>;
20
20
  export declare function restore(io: PublishingIo, root: string, number: string | undefined): Promise<Outcome>;
@@ -3,7 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, st
3
3
  import { homedir, hostname, userInfo } from 'node:os';
4
4
  import { basename, dirname, extname, join, matchesGlob, relative } from 'node:path';
5
5
  import { z } from 'zod';
6
- import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishSummary, sourcePathProblem } from "../publishWire.js";
6
+ import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishSummary, setupCodeExchanged, sourcePathProblem } from "../publishWire.js";
7
7
  import { appInfo } from "../appTitle.js";
8
8
  import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
9
9
  const CONFIG_DIR = join(homedir(), '.config', 'pattern');
@@ -175,6 +175,20 @@ async function signIn(io, again) {
175
175
  text: `Opened ${started.url} in the browser.\n code ${started.code}\n next tell them: "Check the code on the page is ${started.code}, then click Allow." Then, without waiting for them to reply, run ${again} again: it waits for Allow.`
176
176
  };
177
177
  }
178
+ async function useSetupCode(io, code) {
179
+ const response = await fetch(`${dashboard()}/api/auth/cli/setup-codes/exchange`, {
180
+ method: 'POST',
181
+ headers: { 'content-type': 'application/json' },
182
+ body: JSON.stringify({ code, label: labelOfThisMachine() })
183
+ }).catch(() => undefined);
184
+ const exchanged = setupCodeExchanged.safeParse(await response?.json().catch(() => null));
185
+ if (response?.ok === true && exchanged.success) {
186
+ saveCookie(exchanged.data.cookie);
187
+ io.say('✓ Connected with the setup code');
188
+ return;
189
+ }
190
+ io.say('✗ That setup code has expired or was already used');
191
+ }
178
192
  async function signedIn(io, again, work) {
179
193
  if (savedCookie() === undefined) {
180
194
  const outcome = await signIn(io, again);
@@ -237,7 +251,7 @@ function folderFor(name, cwd) {
237
251
  function commandLineToolsReady() {
238
252
  return spawnSync('xcode-select', ['-p'], { stdio: 'ignore' }).status === 0;
239
253
  }
240
- export async function pull(io, name, cwd) {
254
+ export async function pull(io, name, cwd, setupCode) {
241
255
  if (name === undefined || nameProblem(name) !== undefined) {
242
256
  return {
243
257
  code: 2,
@@ -254,6 +268,8 @@ export async function pull(io, name, cwd) {
254
268
  };
255
269
  }
256
270
  try {
271
+ if (setupCode !== undefined)
272
+ await useSetupCode(io, setupCode);
257
273
  const result = await signedIn(io, `app pull ${name}`, async () => {
258
274
  const app = await callJson(`/api/apps/${name}`, appSummary);
259
275
  const response = await call(`/api/apps/${name}/source`);
@@ -99,6 +99,18 @@ export declare const loginToApprove: z.ZodObject<{
99
99
  code: z.ZodString;
100
100
  label: z.ZodString;
101
101
  }, z.core.$strip>;
102
+ export declare const setupCode: z.ZodObject<{
103
+ code: z.ZodString;
104
+ expiresAt: z.ZodString;
105
+ }, z.core.$strip>;
106
+ export declare const setupCodeExchanged: z.ZodObject<{
107
+ cookie: z.ZodString;
108
+ }, z.core.$strip>;
109
+ export declare const connection: z.ZodObject<{
110
+ label: z.ZodString;
111
+ connectedAt: z.ZodString;
112
+ }, z.core.$strip>;
113
+ export type Connection = z.output<typeof connection>;
102
114
  export declare const publishServices: {
103
115
  readonly apps: {
104
116
  readonly list: NoInfer<import("./services.ts").ServiceCall<"/apps", {}, z.ZodArray<z.ZodObject<{
@@ -259,6 +271,19 @@ export declare const publishServices: {
259
271
  finishedAt: z.ZodNullable<z.ZodString>;
260
272
  }, z.core.$strip>>>;
261
273
  };
274
+ readonly setup: {
275
+ readonly newCode: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/setup-code", {}, z.ZodObject<{
276
+ code: z.ZodString;
277
+ expiresAt: z.ZodString;
278
+ }, z.core.$strip>>>;
279
+ readonly connection: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/connection", {}, z.ZodNullable<z.ZodObject<{
280
+ label: z.ZodString;
281
+ connectedAt: z.ZodString;
282
+ }, z.core.$strip>>>>;
283
+ readonly disconnect: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/connection/disconnect", {}, z.ZodObject<{
284
+ ok: z.ZodBoolean;
285
+ }, z.core.$strip>>>;
286
+ };
262
287
  readonly logins: {
263
288
  readonly read: NoInfer<import("./services.ts").ServiceCall<"/cli/logins/:approval", {}, z.ZodObject<{
264
289
  code: z.ZodString;
@@ -126,6 +126,12 @@ export const loginPoll = z.union([
126
126
  z.object({ status: z.literal('approved'), cookie: z.string() })
127
127
  ]);
128
128
  export const loginToApprove = z.object({ code: z.string(), label: z.string() });
129
+ export const setupCode = z.object({ code: z.string(), expiresAt: z.string() });
130
+ export const setupCodeExchanged = z.object({ cookie: z.string() });
131
+ export const connection = z.object({
132
+ label: z.string(),
133
+ connectedAt: z.string()
134
+ });
129
135
  export const publishServices = defineServices({
130
136
  apps: {
131
137
  list: call({ get: '/apps', reply: z.array(appSummary) }),
@@ -158,6 +164,17 @@ export const publishServices = defineServices({
158
164
  reply: publishSummary
159
165
  })
160
166
  },
167
+ setup: {
168
+ newCode: call({ post: '/apps/:name/setup-code', reply: setupCode }),
169
+ connection: call({
170
+ get: '/apps/:name/connection',
171
+ reply: connection.nullable()
172
+ }),
173
+ disconnect: call({
174
+ post: '/apps/:name/connection/disconnect',
175
+ reply: z.object({ ok: z.boolean() })
176
+ })
177
+ },
161
178
  logins: {
162
179
  read: call({ get: '/cli/logins/:approval', reply: loginToApprove }),
163
180
  approve: call({
@@ -5,7 +5,7 @@ export { refused } from '../services.ts';
5
5
  export { serveServices } from './services.ts';
6
6
  export type { ServiceHandlers } from './services.ts';
7
7
  export { sessionCookieFor } from './session.ts';
8
- export { sessions } from './sessions.ts';
8
+ export { deleteSession, sessions } from './sessions.ts';
9
9
  export { readTitle, seedTitleSql, settings, writeTitle } from './settings.ts';
10
10
  export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from './signInWire.ts';
11
11
  export type { VerifiedIdentity } from './signInWire.ts';
@@ -3,7 +3,7 @@ export { seedSql, serveTables, sqlTable, sqlTables } from "./tables.js";
3
3
  export { refused } from "../services.js";
4
4
  export { serveServices } from "./services.js";
5
5
  export { sessionCookieFor } from "./session.js";
6
- export { sessions } from "./sessions.js";
6
+ export { deleteSession, sessions } from "./sessions.js";
7
7
  export { readTitle, seedTitleSql, settings, writeTitle } from "./settings.js";
8
8
  export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from "./signInWire.js";
9
9
  export { findUser, users } from "./users.js";
@@ -17,7 +17,10 @@ export declare function issuePass(c: Context<ServerEnv>, user: StoredUser, sessi
17
17
  recheckAt: number;
18
18
  }>;
19
19
  export declare function startSession(c: Context<ServerEnv>, user: StoredUser): Promise<void>;
20
- export declare function sessionCookieFor(env: ServerEnv['Bindings'], user: StoredUser): Promise<string>;
20
+ export declare function sessionCookieFor(env: ServerEnv['Bindings'], user: StoredUser): Promise<{
21
+ cookie: string;
22
+ sessionId: `${string}-${string}-${string}-${string}-${string}`;
23
+ }>;
21
24
  export declare function readPass(c: Context<ServerEnv>): Promise<{
22
25
  sessionId: string;
23
26
  userId: string;
@@ -37,14 +37,15 @@ export async function startSession(c, user) {
37
37
  await issuePass(c, user, await createSession(c.env.DB, user.id));
38
38
  }
39
39
  export async function sessionCookieFor(env, user) {
40
+ const sessionId = await createSession(env.DB, user.id);
40
41
  const pass = {
41
- sessionId: await createSession(env.DB, user.id),
42
+ sessionId,
42
43
  userId: user.id,
43
44
  email: user.email,
44
45
  recheckAt: nowInSeconds() + RECHECK_SECONDS
45
46
  };
46
47
  const cookie = await serializeSigned(SESSION_COOKIE, JSON.stringify(pass), env.SESSION_SECRET);
47
- return cookie.split(';')[0] ?? cookie;
48
+ return { cookie: cookie.split(';')[0] ?? cookie, sessionId };
48
49
  }
49
50
  export async function readPass(c) {
50
51
  const value = await getSignedCookie(c, c.env.SESSION_SECRET, SESSION_COOKIE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",