@trypattern/sdk 0.1.6 → 0.1.8

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.
@@ -8,4 +8,5 @@ export declare const userSchema: z.ZodObject<{
8
8
  }, z.core.$strip>;
9
9
  export type User = z.output<typeof userSchema>;
10
10
  export type StoredUser = Omit<User, 'isAdmin'>;
11
- export declare const USER_COOKIE = "pattern_user";
11
+ export declare const USER_COOKIE = "__Host-pattern_user";
12
+ export declare const SESSION_COOKIE = "__Host-pattern_session";
package/dist/auth/user.js CHANGED
@@ -6,4 +6,5 @@ export const userSchema = z.object({
6
6
  avatarUrl: z.string().nullable(),
7
7
  isAdmin: z.boolean().default(false)
8
8
  });
9
- export const USER_COOKIE = 'pattern_user';
9
+ export const USER_COOKIE = '__Host-pattern_user';
10
+ export const SESSION_COOKIE = '__Host-pattern_session';
package/dist/cli/app.js CHANGED
@@ -313,8 +313,8 @@ every row and column added since, which version 3's code ignores.`,
313
313
  takesArgument: true
314
314
  },
315
315
  setup: {
316
- summary: 'Verify Node/pnpm major versions, pnpm install, install the global `app` shim',
317
- details: `Idempotent. Fails unless Node and pnpm have the major versions in .nvmrc and package.json devEngines.
316
+ summary: 'Verify Node/pnpm are the pinned major versions or newer, pnpm install, install the global `app` shim',
317
+ details: `Idempotent. Fails when Node or pnpm is older than the major versions in .nvmrc and package.json devEngines.
318
318
  Then runs \`pnpm install\` and installs ${SHIM_PATH}, a tiny shim that walks up from the
319
319
  current directory to the nearest project launcher (an executable ./app containing the line
320
320
  ${LAUNCHER_MARKER_LINE}) and runs it, so each project's CLI lives in its repo and the shim
@@ -1234,6 +1234,9 @@ async function build() {
1234
1234
  function majorOf(version) {
1235
1235
  return version.split('.')[0] ?? '';
1236
1236
  }
1237
+ function isAtLeastMajor(version, pinned) {
1238
+ return version !== '' && Number(majorOf(version)) >= Number(majorOf(pinned));
1239
+ }
1237
1240
  const packagePins = z.object({
1238
1241
  devEngines: z.object({
1239
1242
  packageManager: z.object({ version: z.string() })
@@ -1276,11 +1279,11 @@ async function setup() {
1276
1279
  });
1277
1280
  const havePnpm = (pnpmVersion.stdout ?? '').trim();
1278
1281
  const problems = [];
1279
- if (majorOf(haveNode) !== majorOf(pins.node)) {
1280
- problems.push(`Node ${haveNode} is not Node ${majorOf(pins.node)} (.nvmrc says ${pins.node})\n fix pnpm env use --global ${majorOf(pins.node)}`);
1282
+ if (!isAtLeastMajor(haveNode, pins.node)) {
1283
+ problems.push(`Node ${haveNode} is older than Node ${majorOf(pins.node)} (.nvmrc says ${pins.node})\n fix pnpm env use --global ${majorOf(pins.node)}`);
1281
1284
  }
1282
- if (majorOf(havePnpm) !== majorOf(pins.pnpm)) {
1283
- 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 -`);
1285
+ if (!isAtLeastMajor(havePnpm, pins.pnpm)) {
1286
+ problems.push(`pnpm ${havePnpm || '(not found)'} is older than pnpm ${majorOf(pins.pnpm)} (package.json devEngines says ${pins.pnpm} or newer)\n fix npm install --global pnpm@12`);
1284
1287
  }
1285
1288
  const versions = { node: haveNode, pnpm: havePnpm, expected: pins };
1286
1289
  if (problems.length > 0) {
@@ -1290,7 +1293,10 @@ async function setup() {
1290
1293
  text: problems.map((p) => `✗ ${p}`).join('\n')
1291
1294
  };
1292
1295
  }
1293
- say(`✓ Node ${haveNode} and pnpm ${havePnpm} match the pinned major versions`);
1296
+ const tested = majorOf(haveNode) === majorOf(pins.node)
1297
+ ? ''
1298
+ : ` (tested on Node ${majorOf(pins.node)})`;
1299
+ say(`✓ Node ${haveNode} and pnpm ${havePnpm}${tested}`);
1294
1300
  const install = await capture('pnpm', ['install']);
1295
1301
  if (install.code !== 0) {
1296
1302
  return {
@@ -1,10 +1,11 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
- import { homedir, hostname, userInfo } from 'node:os';
3
+ import { homedir } from 'node:os';
4
4
  import { basename, dirname, extname, join, matchesGlob, relative } from 'node:path';
5
5
  import { z } from 'zod';
6
6
  import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishMessage, publishSummary, setupCodeExchanged, sourcePathProblem } from "../publishWire.js";
7
7
  import { appInfo } from "../appTitle.js";
8
+ import { SESSION_COOKIE } from "../auth/user.js";
8
9
  import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
9
10
  const CONFIG_DIR = join(homedir(), '.config', 'pattern');
10
11
  const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
@@ -13,7 +14,6 @@ const LOGIN_MINUTES = 10;
13
14
  const POLL_MS = 2000;
14
15
  const LOGIN_WAIT_MS = 100_000;
15
16
  const PUBLISH_WAIT_MS = 300_000;
16
- const SESSION_COOKIE = 'pattern_session';
17
17
  const LAUNCHER = 'app';
18
18
  const STARTER_WORKSPACE = 'pnpm-workspace.yaml';
19
19
  const STARTER_BUILD_SETTINGS = 'allowBuilds:\n esbuild: false\n workerd: false\n';
@@ -121,8 +121,11 @@ async function callJson(path, schema, init = {}) {
121
121
  return schema.parse(body);
122
122
  }
123
123
  function labelOfThisMachine() {
124
- const machine = hostname().replace(/\.local$/, '');
125
- return `${userInfo().username} on ${machine}`.slice(0, 80);
124
+ const named = spawnSync('scutil', ['--get', 'ComputerName'], {
125
+ encoding: 'utf8'
126
+ });
127
+ const name = named.status === 0 ? named.stdout.trim() : '';
128
+ return (name === '' ? 'this computer' : name).slice(0, 80);
126
129
  }
127
130
  function sleep(ms) {
128
131
  return new Promise((done) => setTimeout(done, ms));
@@ -248,6 +251,24 @@ function folderFor(name, cwd) {
248
251
  return cwd;
249
252
  return join(isInsideHome(cwd) ? cwd : homedir(), name);
250
253
  }
254
+ async function hasPnpm(io, cwd) {
255
+ return (await io.run('pnpm', ['--version'], cwd)).code === 0;
256
+ }
257
+ async function ensurePnpm(io, cwd) {
258
+ if (await hasPnpm(io, cwd))
259
+ return true;
260
+ io.say('Installing pnpm');
261
+ const installed = await io.run('npm', ['install', '--global', 'pnpm@12'], cwd);
262
+ return installed.code === 0 && (await hasPnpm(io, cwd));
263
+ }
264
+ function firstChange(title, url, folder) {
265
+ return [
266
+ `cd into ${folder} and read AGENTS.md: every rule in it applies. Then run ./app start and open the address it prints, so they see their site running on this computer.`,
267
+ `Ask them one question: "In a sentence, what does ${title} do?" From their answer, make the home page welcome them with a line that says what ${title} does, and give src/theme/theme.ts a brand colour that suits the business.`,
268
+ `Run ./app check until it passes, then ./app publish --message '<what changed>' straight away: this first change goes live without asking, so they see their real site change at ${url}.`,
269
+ `Then tell them their site lives in ${folder}, that next time they can start Claude there, and that from now on you'll show changes on this computer first and publish when they say so.`
270
+ ].join('\n ');
271
+ }
251
272
  function commandLineToolsReady() {
252
273
  return spawnSync('xcode-select', ['-p'], { stdio: 'ignore' }).status === 0;
253
274
  }
@@ -276,12 +297,13 @@ export async function pull(io, name, cwd, setupCode) {
276
297
  if (!response.ok)
277
298
  throw new Error(`Couldn’t download ${name}`);
278
299
  const number = response.headers.get('x-pattern-publish') ?? '?';
300
+ const isStarter = response.headers.get('x-pattern-starter') === 'true';
279
301
  const live = await fetch(`${app.url}/api/app`)
280
302
  .then((reply) => reply.json())
281
303
  .then((body) => appInfo.parse(body).title)
282
304
  .catch(() => '');
283
305
  const title = live === '' ? app.title : live;
284
- return { app, title, number, form: await response.formData() };
306
+ return { app, title, number, isStarter, form: await response.formData() };
285
307
  });
286
308
  if (isOutcome(result))
287
309
  return result;
@@ -301,6 +323,13 @@ export async function pull(io, name, cwd, setupCode) {
301
323
  chmodSync(join(target, LAUNCHER), 0o755);
302
324
  writeFileSync(join(target, PROJECT_FILE), `${JSON.stringify({ name, title: result.title }, null, 2)}\n`);
303
325
  io.say(`✓ Downloaded ${files} files of version ${result.number}`);
326
+ if (!(await ensurePnpm(io, target))) {
327
+ return {
328
+ code: 1,
329
+ data: { error: 'pnpm missing', folder: target },
330
+ text: `✗ pnpm isn’t installed, and npm couldn’t install it\n fix curl -fsSL https://get.pnpm.io/install.sh | sh -, then open a new terminal and run this again`
331
+ };
332
+ }
304
333
  const install = await io.run('pnpm', ['install'], target);
305
334
  if (install.code !== 0) {
306
335
  return failure(new Error(`pnpm install failed\n${install.output}`));
@@ -314,11 +343,13 @@ export async function pull(io, name, cwd, setupCode) {
314
343
  await io.run('git', ['add', '-A'], target);
315
344
  await io.run('git', ['commit', '-q', '-m', `Version ${result.number} from onpattern.app`], target);
316
345
  }
317
- const moved = `\n folder ${target}`;
346
+ const next = result.isStarter
347
+ ? firstChange(result.title, result.app.url, target)
348
+ : `cd into ${target}, read AGENTS.md, then ./app start`;
318
349
  return {
319
350
  code: 0,
320
351
  data: { name, folder: target, version: result.number, files },
321
- text: `✓ ${result.title} is on this computer${moved}\n next cd into that folder, read AGENTS.md, then ./app start`
352
+ text: `✓ ${result.title} is on this computer\n folder ${target}\n next ${next}`
322
353
  };
323
354
  }
324
355
  catch (error) {
@@ -8,7 +8,7 @@ import { Icon } from "./Icon.js";
8
8
  import { interact, textOf } from "./interaction.js";
9
9
  import { Kbd } from "./Kbd.js";
10
10
  import { Spinner } from "./Spinner.js";
11
- export function Button({ children, variant = 'primary', tone = 'neutral', size = 'md', to, href, icon, trailingIcon, shortcut, type = 'button', disabled = false, isPending = false, isFullWidth = false, onClick, style }) {
11
+ export function Button({ children, variant = 'primary', tone = variant === 'primary' ? 'brand' : 'neutral', size = 'md', to, href, icon, trailingIcon, shortcut, type = 'button', disabled = false, isPending = false, isFullWidth = false, onClick, style }) {
12
12
  const isDisabled = disabled || isPending;
13
13
  const report = () => {
14
14
  interact({ primitive: 'Button', event: 'click', label: textOf(children) });
@@ -89,8 +89,8 @@ const styles = stylex.create({
89
89
  placeItems: 'center',
90
90
  width: iconSizes.xl,
91
91
  height: iconSizes.xl,
92
- color: colors.onInvertedForeground,
93
- backgroundColor: colors.invertedSolid,
92
+ color: colors.onBrandSolidForeground,
93
+ backgroundColor: colors.brandSolid,
94
94
  borderRadius: radii.md
95
95
  },
96
96
  body: {
@@ -33,8 +33,8 @@ const styles = stylex.create({
33
33
  alignSelf: 'center',
34
34
  width: controls.heightLg,
35
35
  height: controls.heightLg,
36
- color: colors.onInvertedForeground,
37
- backgroundColor: colors.invertedSolid,
36
+ color: colors.onBrandSolidForeground,
37
+ backgroundColor: colors.brandSolid,
38
38
  borderRadius: radii.lg
39
39
  }
40
40
  });
@@ -6,10 +6,10 @@ import { forgetSession, readPass, startSession } from "./session.js";
6
6
  import { readTitle } from "./settings.js";
7
7
  import { deleteSession } from "./sessions.js";
8
8
  import { saveUser } from "./users.js";
9
- const GOOGLE_COOKIE = 'pattern_google';
10
- const EMAIL_COOKIE = 'pattern_email';
9
+ const GOOGLE_COOKIE = '__Host-pattern_google';
10
+ const EMAIL_COOKIE = '__Host-pattern_email';
11
11
  const signInCookie = {
12
- path: '/api/auth',
12
+ path: '/',
13
13
  httpOnly: true,
14
14
  secure: true,
15
15
  sameSite: 'Lax',
@@ -1,9 +1,8 @@
1
1
  import { deleteCookie, getSignedCookie, setCookie, setSignedCookie } from 'hono/cookie';
2
2
  import { serializeSigned } from 'hono/utils/cookie';
3
3
  import { z } from 'zod';
4
- import { USER_COOKIE } from "../auth/user.js";
4
+ import { SESSION_COOKIE, USER_COOKIE } from "../auth/user.js";
5
5
  import { createSession } from "./sessions.js";
6
- const SESSION_COOKIE = 'pattern_session';
7
6
  const RECHECK_SECONDS = 15 * 60;
8
7
  const SESSION_SECONDS = 30 * 86_400;
9
8
  const cookieOptions = {
@@ -61,6 +60,6 @@ export async function readPass(c) {
61
60
  }
62
61
  export const needsRecheck = (pass) => pass.recheckAt <= nowInSeconds();
63
62
  export function forgetSession(c) {
64
- deleteCookie(c, SESSION_COOKIE, { path: '/' });
65
- deleteCookie(c, USER_COOKIE, { path: '/' });
63
+ deleteCookie(c, SESSION_COOKIE, { path: '/', secure: true });
64
+ deleteCookie(c, USER_COOKIE, { path: '/', secure: true });
66
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",