@trypattern/sdk 0.1.5 → 0.1.7

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
@@ -74,7 +74,13 @@ const CONTACT_SHEET = join(SCREENSHOTS_DIR, 'contact-sheet.png');
74
74
  const SCREENSHOT_WORKER = join(SDK_SOURCE, 'cli', `screenshot${SDK_EXTENSION}`);
75
75
  const VISUAL_URL_ENV = 'PATTERN_VISUAL_URL';
76
76
  const GLOBAL_FLAGS = ['--json', '--help', '-h'];
77
- const VALUE_FLAGS = new Set(['--commands', '--name', '--as', '--code']);
77
+ const VALUE_FLAGS = new Set([
78
+ '--commands',
79
+ '--name',
80
+ '--as',
81
+ '--code',
82
+ '--message'
83
+ ]);
78
84
  const SHIM_SOURCE = `#!/bin/sh
79
85
  ${SHIM_MARKER_LINE}
80
86
  dir=$PWD
@@ -284,9 +290,14 @@ git would keep, never .dev.vars, .env, node_modules, dist or .dev) to onpattern.
284
290
  until the new version answers at https://<name>.onpattern.app, printing each step. The database
285
291
  moves forward with any new migrations; nothing is ever deleted from it. If a step fails, the site
286
292
  keeps running the previous version and the error says why. Run app check first.
293
+ --message '…' says what changed in a few plain words for the owner ("Pink brand colour and a welcome
294
+ line"); onpattern.app and app versions show it beside the version.
287
295
  --starter (Pattern's admins only) publishes this project as the starter every new site begins from.`,
288
- run: (_argument, options) => publish(publishingIo(), ROOT, options.has('--starter')),
289
- flags: ['--starter']
296
+ run: (_argument, options) => {
297
+ const message = options.get('--message');
298
+ return publish(publishingIo(), ROOT, options.has('--starter'), typeof message === 'string' ? message : undefined);
299
+ },
300
+ flags: ['--starter', '--message']
290
301
  },
291
302
  versions: {
292
303
  summary: 'List the versions that went live, newest first',
@@ -302,8 +313,8 @@ every row and column added since, which version 3's code ignores.`,
302
313
  takesArgument: true
303
314
  },
304
315
  setup: {
305
- summary: 'Verify Node/pnpm major versions, pnpm install, install the global `app` shim',
306
- 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.
307
318
  Then runs \`pnpm install\` and installs ${SHIM_PATH}, a tiny shim that walks up from the
308
319
  current directory to the nearest project launcher (an executable ./app containing the line
309
320
  ${LAUNCHER_MARKER_LINE}) and runs it, so each project's CLI lives in its repo and the shim
@@ -1223,6 +1234,9 @@ async function build() {
1223
1234
  function majorOf(version) {
1224
1235
  return version.split('.')[0] ?? '';
1225
1236
  }
1237
+ function isAtLeastMajor(version, pinned) {
1238
+ return version !== '' && Number(majorOf(version)) >= Number(majorOf(pinned));
1239
+ }
1226
1240
  const packagePins = z.object({
1227
1241
  devEngines: z.object({
1228
1242
  packageManager: z.object({ version: z.string() })
@@ -1265,11 +1279,11 @@ async function setup() {
1265
1279
  });
1266
1280
  const havePnpm = (pnpmVersion.stdout ?? '').trim();
1267
1281
  const problems = [];
1268
- if (majorOf(haveNode) !== majorOf(pins.node)) {
1269
- 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)}`);
1270
1284
  }
1271
- if (majorOf(havePnpm) !== majorOf(pins.pnpm)) {
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 -`);
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@latest`);
1273
1287
  }
1274
1288
  const versions = { node: haveNode, pnpm: havePnpm, expected: pins };
1275
1289
  if (problems.length > 0) {
@@ -1279,7 +1293,10 @@ async function setup() {
1279
1293
  text: problems.map((p) => `✗ ${p}`).join('\n')
1280
1294
  };
1281
1295
  }
1282
- 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}`);
1283
1300
  const install = await capture('pnpm', ['install']);
1284
1301
  if (install.code !== 0) {
1285
1302
  return {
@@ -1311,7 +1328,7 @@ async function setup() {
1311
1328
  function help() {
1312
1329
  const width = Math.max(...Object.keys(COMMANDS).map((n) => n.length));
1313
1330
  const lines = Object.entries(COMMANDS).map(([name, cmd]) => `app ${name.padEnd(width)} ${cmd.summary}`);
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)');
1331
+ 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, publish --message)');
1315
1332
  return {
1316
1333
  code: 0,
1317
1334
  data: {
@@ -15,7 +15,7 @@ export type PublishingIo = {
15
15
  };
16
16
  export declare function login(io: PublishingIo): Promise<Outcome>;
17
17
  export declare function pull(io: PublishingIo, name: string | undefined, cwd: string, setupCode: string | undefined): Promise<Outcome>;
18
- export declare function publish(io: PublishingIo, root: string, isStarter: boolean): Promise<Outcome>;
18
+ export declare function publish(io: PublishingIo, root: string, isStarter: boolean, message: string | undefined): 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>;
21
21
  export {};
@@ -1,9 +1,9 @@
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
- import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, nameProblem, publishSummary, setupCodeExchanged, sourcePathProblem } from "../publishWire.js";
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
8
  import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
9
9
  const CONFIG_DIR = join(homedir(), '.config', 'pattern');
@@ -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@latest'], 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) {
@@ -424,11 +455,13 @@ async function followPublish(io, name, started) {
424
455
  }
425
456
  const data = { name, version: publish.number, status: publish.status };
426
457
  if (publish.status === 'live') {
427
- return {
428
- code: 0,
429
- data,
430
- text: `✓ Version ${publish.number} is live at https://${name}.onpattern.app`
431
- };
458
+ const lines = [
459
+ `✓ Version ${publish.number} is live at https://${name}.onpattern.app${publish.message === null ? '' : ` · ${publish.message}`}`
460
+ ];
461
+ if (publish.message === null && publish.restoredFrom === null) {
462
+ lines.push(" tip add --message '…' so the owner sees what changed, in plain words");
463
+ }
464
+ return { code: 0, data, text: lines.join('\n') };
432
465
  }
433
466
  if (publish.status === 'failed') {
434
467
  return {
@@ -443,7 +476,7 @@ async function followPublish(io, name, started) {
443
476
  text: `✗ Version ${publish.number} is still publishing after ${PUBLISH_WAIT_MS / 60_000} minutes\n next app versions shows how it ends`
444
477
  };
445
478
  }
446
- export async function publish(io, root, isStarter) {
479
+ export async function publish(io, root, isStarter, message) {
447
480
  try {
448
481
  const { name } = readProjectFile(root);
449
482
  const built = await io.build();
@@ -451,6 +484,9 @@ export async function publish(io, root, isStarter) {
451
484
  return built;
452
485
  io.say('✓ Built');
453
486
  const { form, sources } = uploadForm(root, io, isStarter);
487
+ const said = publishMessage(message ?? '');
488
+ if (said !== null)
489
+ form.append(UPLOAD.message, said);
454
490
  io.say(`✓ Packed the build and ${sources} source files`);
455
491
  const again = isStarter ? 'app publish --starter' : 'app publish';
456
492
  const result = await signedIn(io, again, async () => {
@@ -492,7 +528,7 @@ export async function versions(io, root) {
492
528
  ? `restored version ${item.restoredFrom}`
493
529
  : item.isStarter
494
530
  ? 'starter site'
495
- : `published by ${item.byEmail}`;
531
+ : (item.message ?? `published by ${item.byEmail}`);
496
532
  return ` ${String(item.number).padStart(3)} ${item.status.padEnd(7)} ${item.startedAt.slice(0, 16).replace('T', ' ')} ${what}`;
497
533
  });
498
534
  return {
@@ -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
  });
@@ -17,7 +17,10 @@ export declare const UPLOAD: {
17
17
  readonly source: "source/";
18
18
  readonly compatibilityDate: "compatibilityDate";
19
19
  readonly sdkVersion: "sdkVersion";
20
+ readonly message: "message";
20
21
  };
22
+ export declare const PUBLISH_MESSAGE_LIMIT = 100;
23
+ export declare function publishMessage(text: string): string | null;
21
24
  export declare const publishStep: z.ZodEnum<{
22
25
  publish: "publish";
23
26
  reserve: "reserve";
@@ -47,6 +50,7 @@ export declare const publishSummary: z.ZodObject<{
47
50
  byEmail: z.ZodString;
48
51
  restoredFrom: z.ZodNullable<z.ZodNumber>;
49
52
  isStarter: z.ZodBoolean;
53
+ message: z.ZodNullable<z.ZodString>;
50
54
  startedAt: z.ZodString;
51
55
  finishedAt: z.ZodNullable<z.ZodString>;
52
56
  }, z.core.$strip>;
@@ -73,6 +77,7 @@ export declare const appSummary: z.ZodObject<{
73
77
  byEmail: z.ZodString;
74
78
  restoredFrom: z.ZodNullable<z.ZodNumber>;
75
79
  isStarter: z.ZodBoolean;
80
+ message: z.ZodNullable<z.ZodString>;
76
81
  startedAt: z.ZodString;
77
82
  finishedAt: z.ZodNullable<z.ZodString>;
78
83
  }, z.core.$strip>>;
@@ -135,6 +140,7 @@ export declare const publishServices: {
135
140
  byEmail: z.ZodString;
136
141
  restoredFrom: z.ZodNullable<z.ZodNumber>;
137
142
  isStarter: z.ZodBoolean;
143
+ message: z.ZodNullable<z.ZodString>;
138
144
  startedAt: z.ZodString;
139
145
  finishedAt: z.ZodNullable<z.ZodString>;
140
146
  }, z.core.$strip>>;
@@ -161,6 +167,7 @@ export declare const publishServices: {
161
167
  byEmail: z.ZodString;
162
168
  restoredFrom: z.ZodNullable<z.ZodNumber>;
163
169
  isStarter: z.ZodBoolean;
170
+ message: z.ZodNullable<z.ZodString>;
164
171
  startedAt: z.ZodString;
165
172
  finishedAt: z.ZodNullable<z.ZodString>;
166
173
  }, z.core.$strip>>;
@@ -190,6 +197,7 @@ export declare const publishServices: {
190
197
  byEmail: z.ZodString;
191
198
  restoredFrom: z.ZodNullable<z.ZodNumber>;
192
199
  isStarter: z.ZodBoolean;
200
+ message: z.ZodNullable<z.ZodString>;
193
201
  startedAt: z.ZodString;
194
202
  finishedAt: z.ZodNullable<z.ZodString>;
195
203
  }, z.core.$strip>>;
@@ -227,6 +235,7 @@ export declare const publishServices: {
227
235
  byEmail: z.ZodString;
228
236
  restoredFrom: z.ZodNullable<z.ZodNumber>;
229
237
  isStarter: z.ZodBoolean;
238
+ message: z.ZodNullable<z.ZodString>;
230
239
  startedAt: z.ZodString;
231
240
  finishedAt: z.ZodNullable<z.ZodString>;
232
241
  }, z.core.$strip>>>>;
@@ -247,6 +256,7 @@ export declare const publishServices: {
247
256
  byEmail: z.ZodString;
248
257
  restoredFrom: z.ZodNullable<z.ZodNumber>;
249
258
  isStarter: z.ZodBoolean;
259
+ message: z.ZodNullable<z.ZodString>;
250
260
  startedAt: z.ZodString;
251
261
  finishedAt: z.ZodNullable<z.ZodString>;
252
262
  }, z.core.$strip>>>;
@@ -267,6 +277,7 @@ export declare const publishServices: {
267
277
  byEmail: z.ZodString;
268
278
  restoredFrom: z.ZodNullable<z.ZodNumber>;
269
279
  isStarter: z.ZodBoolean;
280
+ message: z.ZodNullable<z.ZodString>;
270
281
  startedAt: z.ZodString;
271
282
  finishedAt: z.ZodNullable<z.ZodString>;
272
283
  }, z.core.$strip>>>;
@@ -89,8 +89,14 @@ export const UPLOAD = {
89
89
  migrations: 'migrations/',
90
90
  source: 'source/',
91
91
  compatibilityDate: 'compatibilityDate',
92
- sdkVersion: 'sdkVersion'
92
+ sdkVersion: 'sdkVersion',
93
+ message: 'message'
93
94
  };
95
+ export const PUBLISH_MESSAGE_LIMIT = 100;
96
+ export function publishMessage(text) {
97
+ const firstLine = (text.split('\n')[0] ?? '').replaceAll(/\s+/g, ' ').trim();
98
+ return firstLine === '' ? null : firstLine.slice(0, PUBLISH_MESSAGE_LIMIT);
99
+ }
94
100
  export const publishStep = z.enum(['reserve', 'database', 'sign-in', 'publish']);
95
101
  export const publishStatus = z.enum(['running', 'live', 'failed']);
96
102
  export const publishSummary = z.object({
@@ -101,6 +107,7 @@ export const publishSummary = z.object({
101
107
  byEmail: z.string(),
102
108
  restoredFrom: z.number().nullable(),
103
109
  isStarter: z.boolean(),
110
+ message: z.string().nullable(),
104
111
  startedAt: z.string(),
105
112
  finishedAt: z.string().nullable()
106
113
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",