@trypattern/sdk 0.1.21 → 0.1.23
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 +83 -1
- package/dist/cli/data.d.ts +5 -0
- package/dist/cli/data.js +273 -0
- package/dist/cli/publishing.d.ts +9 -2
- package/dist/cli/publishing.js +7 -7
- package/dist/cli/updates.d.ts +4 -0
- package/dist/cli/updates.js +68 -0
- package/dist/components/Badge.d.ts +1 -1
- package/dist/components/Badge.js +18 -0
- package/dist/components/ButtonBody.d.ts +1 -1
- package/dist/components/ButtonBody.js +43 -0
- package/dist/patternAccess.d.ts +2 -0
- package/dist/patternAccess.js +2 -1
- package/dist/publishWire.d.ts +355 -1
- package/dist/publishWire.js +66 -1
- package/dist/server/changes.d.ts +18 -0
- package/dist/server/changes.js +113 -0
- package/dist/server/createServer.d.ts +38 -0
- package/dist/server/database.d.ts +38 -0
- package/dist/server/database.js +2 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/patternViewer.js +4 -1
- package/dist/server/session.d.ts +1 -0
- package/dist/server/session.js +3 -0
- package/package.json +1 -1
package/dist/cli/app.js
CHANGED
|
@@ -20,7 +20,10 @@ import { putLocalPhotos, resetLocalDatabase } from "../vite/localWorker.js";
|
|
|
20
20
|
import { readProjectFile } from "../vite/projectFile.js";
|
|
21
21
|
import { readStarter, starterContentOf, withStarter } from "./starter.js";
|
|
22
22
|
import { addLiveMedia, login, publish, pull, restore, versions as listVersions } from "./publishing.js";
|
|
23
|
+
import { CHANGE_SET_ROWS } from "../publishWire.js";
|
|
24
|
+
import { DATA_USAGE, data } from "./data.js";
|
|
23
25
|
import { toolBin } from "./tools.js";
|
|
26
|
+
import { isNewer, latestPattern, newerPattern, updateNote } from "./updates.js";
|
|
24
27
|
function dependsOnPattern(directory) {
|
|
25
28
|
try {
|
|
26
29
|
const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
@@ -78,13 +81,18 @@ const CONTACT_SHEET = join(SCREENSHOTS_DIR, 'contact-sheet.png');
|
|
|
78
81
|
const SCREENSHOT_WORKER = join(SDK_SOURCE, 'cli', `screenshot${SDK_EXTENSION}`);
|
|
79
82
|
const VISUAL_URL_ENV = 'PATTERN_VISUAL_URL';
|
|
80
83
|
const GLOBAL_FLAGS = ['--json', '--help', '-h'];
|
|
84
|
+
const NUDGED_COMMANDS = new Set(['start', 'check', 'publish', 'data']);
|
|
81
85
|
const VALUE_FLAGS = new Set([
|
|
82
86
|
'--commands',
|
|
83
87
|
'--name',
|
|
84
88
|
'--as',
|
|
85
89
|
'--code',
|
|
86
90
|
'--message',
|
|
87
|
-
'--alt'
|
|
91
|
+
'--alt',
|
|
92
|
+
'--row',
|
|
93
|
+
'--set',
|
|
94
|
+
'--file',
|
|
95
|
+
'--asked'
|
|
88
96
|
]);
|
|
89
97
|
const PHOTO_TYPES = {
|
|
90
98
|
'.jpg': 'image/jpeg',
|
|
@@ -349,6 +357,33 @@ own POST /api/media, then the rows. Later publishes never touch them; they are t
|
|
|
349
357
|
},
|
|
350
358
|
flags: ['--starter', '--message']
|
|
351
359
|
},
|
|
360
|
+
data: {
|
|
361
|
+
summary: 'Change the live site’s data, with the owner’s approval (app data --help)',
|
|
362
|
+
details: `${DATA_USAGE}
|
|
363
|
+
The live database belongs to the owner: changes to it go to them for approval, never straight in.
|
|
364
|
+
tables and list read the live data (columns, ids and values) through onpattern.app. add, update
|
|
365
|
+
and remove only collect changes on this computer (.dev/data-changes.json); nothing live changes.
|
|
366
|
+
Values use the table's own columns and types, as tables shows them: a price stored in cents is 1400.
|
|
367
|
+
send puts every collected change in front of the owner as one request on onpattern.app (Database)
|
|
368
|
+
and emails them. --asked is required: their request in their own words, quoted, which they see
|
|
369
|
+
beside Approve. Only the owner can approve, in their browser; send then waits for their decision
|
|
370
|
+
and prints what went live. A row the owner changed after you read it is skipped, never
|
|
371
|
+
overwritten: list it again and send a new change. The live site must run a Pattern with app data:
|
|
372
|
+
after updating @trypattern/sdk, app publish once first. status waits on the latest request; clear drops
|
|
373
|
+
collected changes. Up to ${CHANGE_SET_ROWS} changes per request. For this computer's data, edit seed.ts and run app reset.`,
|
|
374
|
+
run: (_argument, options, rest) => data(publishingIo(), ROOT, rest, options),
|
|
375
|
+
argumentCount: 3,
|
|
376
|
+
flags: ['--row', '--set', '--file', '--asked']
|
|
377
|
+
},
|
|
378
|
+
update: {
|
|
379
|
+
summary: 'Bring this project to the newest Pattern',
|
|
380
|
+
details: `Installs the newest @trypattern/sdk from npm into this project (pnpm add, exact version).
|
|
381
|
+
start, check, publish and data say when a newer Pattern is out. The live site keeps the old one until
|
|
382
|
+
the next app publish, so run app check, then publish it (straight away when nothing else is in
|
|
383
|
+
progress). New Pattern features on the live site, such as the owner approving app data changes,
|
|
384
|
+
need that publish.`,
|
|
385
|
+
run: update
|
|
386
|
+
},
|
|
352
387
|
versions: {
|
|
353
388
|
summary: 'List the versions that went live, newest first',
|
|
354
389
|
details: 'Each version with its number, whether it went live, when, and who published it. app restore <number> puts one back.',
|
|
@@ -1468,6 +1503,43 @@ function installShim() {
|
|
|
1468
1503
|
path: SHIM_PATH
|
|
1469
1504
|
};
|
|
1470
1505
|
}
|
|
1506
|
+
async function update() {
|
|
1507
|
+
const current = sdkVersion();
|
|
1508
|
+
const latest = await latestPattern(true);
|
|
1509
|
+
if (latest === undefined) {
|
|
1510
|
+
return {
|
|
1511
|
+
code: 1,
|
|
1512
|
+
data: { current, error: 'npm did not answer' },
|
|
1513
|
+
text: '✗ Couldn’t reach npm to find the newest Pattern. Check the internet connection and try again.'
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
if (!isNewer(latest, current)) {
|
|
1517
|
+
return {
|
|
1518
|
+
code: 0,
|
|
1519
|
+
data: { current, latest, updated: false },
|
|
1520
|
+
text: `✓ Pattern is up to date (${current})`
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
say(`Updating Pattern ${current} → ${latest}`);
|
|
1524
|
+
const installed = await capture('pnpm', ['add', `@trypattern/sdk@${latest}`]);
|
|
1525
|
+
if (installed.code !== 0) {
|
|
1526
|
+
return {
|
|
1527
|
+
code: 1,
|
|
1528
|
+
data: {
|
|
1529
|
+
current,
|
|
1530
|
+
latest,
|
|
1531
|
+
error: 'pnpm add failed',
|
|
1532
|
+
output: installed.output
|
|
1533
|
+
},
|
|
1534
|
+
text: `✗ pnpm couldn’t install Pattern ${latest}:\n${installed.output.trim().split('\n').slice(-LOG_TAIL_LINES).join('\n')}`
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
code: 0,
|
|
1539
|
+
data: { current, latest, updated: true },
|
|
1540
|
+
text: `✓ Updated Pattern ${current} → ${latest}\n next run app check, then app publish --message 'Updated Pattern' (straight away if nothing else is in progress) so the live site gets it`
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1471
1543
|
async function setup() {
|
|
1472
1544
|
const pins = readPins();
|
|
1473
1545
|
const haveNode = process.versions.node;
|
|
@@ -1611,6 +1683,16 @@ async function main(argv) {
|
|
|
1611
1683
|
}
|
|
1612
1684
|
else {
|
|
1613
1685
|
outcome = await command.run(positional[1], options, positional.slice(1));
|
|
1686
|
+
if (NUDGED_COMMANDS.has(name)) {
|
|
1687
|
+
const latest = await newerPattern(sdkVersion());
|
|
1688
|
+
if (latest !== undefined) {
|
|
1689
|
+
outcome = {
|
|
1690
|
+
...outcome,
|
|
1691
|
+
data: { ...outcome.data, newerPattern: latest },
|
|
1692
|
+
text: `${outcome.text}\n${updateNote(latest, sdkVersion())}`
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1614
1696
|
}
|
|
1615
1697
|
if (jsonMode) {
|
|
1616
1698
|
const payload = {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Outcome, PublishingIo } from './publishing.ts';
|
|
2
|
+
export declare const DATA_USAGE = "app data tables\napp data list <table>\napp data add <table> --row '{\"name\":\"Gold chair\",\"price\":8}' (or --file rows.json, a JSON array)\napp data update <table> <id> --set '{\"price\":14}'\napp data remove <table> <id>\napp data send --asked \"<the owner's request, in their words>\"\napp data status\napp data clear";
|
|
3
|
+
type DataOptions = ReadonlyMap<string, string | true>;
|
|
4
|
+
export declare function data(io: PublishingIo, root: string, [action, table, id]: readonly (string | undefined)[], options: DataOptions): Promise<Outcome>;
|
|
5
|
+
export {};
|
package/dist/cli/data.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { CHANGE_SET_ROWS, DASHBOARD_URL, changeSet, dataChange, databaseRow, databaseRows, databaseSchema } from "../publishWire.js";
|
|
5
|
+
import { readProjectFile } from "../vite/projectFile.js";
|
|
6
|
+
import { callJson, failure, isOutcome, sayHost, signedIn, sleep } from "./publishing.js";
|
|
7
|
+
const STAGED = join('.dev', 'data-changes.json');
|
|
8
|
+
const WAIT_MS = 90_000;
|
|
9
|
+
const POLL_MS = 3000;
|
|
10
|
+
const MAX_PAGES = 50;
|
|
11
|
+
const staged = z.array(dataChange);
|
|
12
|
+
export const DATA_USAGE = `app data tables
|
|
13
|
+
app data list <table>
|
|
14
|
+
app data add <table> --row '{"name":"Gold chair","price":8}' (or --file rows.json, a JSON array)
|
|
15
|
+
app data update <table> <id> --set '{"price":14}'
|
|
16
|
+
app data remove <table> <id>
|
|
17
|
+
app data send --asked "<the owner's request, in their words>"
|
|
18
|
+
app data status
|
|
19
|
+
app data clear`;
|
|
20
|
+
function stagedPath(root) {
|
|
21
|
+
return join(root, STAGED);
|
|
22
|
+
}
|
|
23
|
+
function readStaged(root) {
|
|
24
|
+
const path = stagedPath(root);
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return [];
|
|
27
|
+
const parsed = staged.safeParse(JSON.parse(readFileSync(path, 'utf8')));
|
|
28
|
+
return parsed.success ? parsed.data : [];
|
|
29
|
+
}
|
|
30
|
+
function writeStaged(root, changes) {
|
|
31
|
+
const path = stagedPath(root);
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
writeFileSync(path, `${JSON.stringify(changes, null, 2)}\n`);
|
|
34
|
+
}
|
|
35
|
+
function refusal(text, code = 1) {
|
|
36
|
+
return { code, data: { error: text }, text: `✗ ${text}` };
|
|
37
|
+
}
|
|
38
|
+
function valueOf(options, flag) {
|
|
39
|
+
const value = options.get(flag);
|
|
40
|
+
return typeof value === 'string' ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
function jsonOf(text, what) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error(`${what} is not JSON: ${text.slice(0, 80)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function rowsGiven(root, options) {
|
|
51
|
+
const row = valueOf(options, '--row');
|
|
52
|
+
const file = valueOf(options, '--file');
|
|
53
|
+
if (row !== undefined)
|
|
54
|
+
return [databaseRow.parse(jsonOf(row, '--row'))];
|
|
55
|
+
if (file === undefined)
|
|
56
|
+
throw new Error('Give the row with --row or --file');
|
|
57
|
+
const path = resolve(root, file);
|
|
58
|
+
if (!existsSync(path))
|
|
59
|
+
throw new Error(`There is no file at ${file}`);
|
|
60
|
+
return z.array(databaseRow).parse(jsonOf(readFileSync(path, 'utf8'), file));
|
|
61
|
+
}
|
|
62
|
+
function pendingLines(changes) {
|
|
63
|
+
return changes.map((change) => {
|
|
64
|
+
if (change.op === 'add')
|
|
65
|
+
return ` + ${change.table} ${JSON.stringify(change.row)}`;
|
|
66
|
+
if (change.op === 'remove')
|
|
67
|
+
return ` − ${change.table} ${change.id}`;
|
|
68
|
+
return ` ~ ${change.table} ${change.id} ${JSON.stringify(change.after)}`;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function liveRows(io, name, table) {
|
|
72
|
+
const rows = [];
|
|
73
|
+
let after = null;
|
|
74
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
75
|
+
const query = after === null ? '' : `?after=${encodeURIComponent(after)}`;
|
|
76
|
+
const result = await signedIn(io, `app data list ${table}`, () => callJson(`/api/apps/${name}/database/${table}${query}`, databaseRows));
|
|
77
|
+
if (isOutcome(result))
|
|
78
|
+
return result;
|
|
79
|
+
rows.push(...result.rows);
|
|
80
|
+
after = result.next;
|
|
81
|
+
if (after === null)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return rows;
|
|
85
|
+
}
|
|
86
|
+
async function tables(io, name) {
|
|
87
|
+
const result = await signedIn(io, 'app data tables', () => callJson(`/api/apps/${name}/database`, databaseSchema));
|
|
88
|
+
if (isOutcome(result))
|
|
89
|
+
return result;
|
|
90
|
+
if (result.status === 'outdated') {
|
|
91
|
+
return refusal('Publish once with the latest Pattern to see the live data');
|
|
92
|
+
}
|
|
93
|
+
const lines = result.tables
|
|
94
|
+
.filter((table) => !table.isBuiltIn)
|
|
95
|
+
.map((table) => {
|
|
96
|
+
const properties = Object.keys(Object(Reflect.get(table.schema, 'properties')));
|
|
97
|
+
return ` ${table.name} ${table.rowCount} rows columns: ${properties.join(', ')}`;
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
code: 0,
|
|
101
|
+
data: { name, tables: result.tables },
|
|
102
|
+
text: [`Live tables of ${name}`, ...lines].join('\n')
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function list(io, name, table) {
|
|
106
|
+
const rows = await liveRows(io, name, table);
|
|
107
|
+
if (isOutcome(rows))
|
|
108
|
+
return rows;
|
|
109
|
+
return {
|
|
110
|
+
code: 0,
|
|
111
|
+
data: { name, table, rows },
|
|
112
|
+
text: [
|
|
113
|
+
`${rows.length} live ${table} rows on ${name}`,
|
|
114
|
+
...rows.map((row) => ` ${JSON.stringify(row)}`)
|
|
115
|
+
].join('\n')
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function stage(root, added) {
|
|
119
|
+
const changes = [...readStaged(root), ...added];
|
|
120
|
+
if (changes.length > CHANGE_SET_ROWS) {
|
|
121
|
+
return refusal(`That's more than ${CHANGE_SET_ROWS} changes. Send these first with app data send, then add the rest.`);
|
|
122
|
+
}
|
|
123
|
+
writeStaged(root, changes);
|
|
124
|
+
return {
|
|
125
|
+
code: 0,
|
|
126
|
+
data: { staged: changes },
|
|
127
|
+
text: [
|
|
128
|
+
`✓ ${changes.length} change${changes.length === 1 ? '' : 's'} ready to send. Nothing on the live site has changed.`,
|
|
129
|
+
...pendingLines(changes),
|
|
130
|
+
' next add any other changes, then app data send --asked "<their request, in their words>"'
|
|
131
|
+
].join('\n')
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function before(io, name, table, id) {
|
|
135
|
+
const rows = await liveRows(io, name, table);
|
|
136
|
+
if (isOutcome(rows))
|
|
137
|
+
return rows;
|
|
138
|
+
const row = rows.find((candidate) => candidate['id'] === id);
|
|
139
|
+
if (row === undefined)
|
|
140
|
+
throw new Error(`There is no ${table} row with id ${id}`);
|
|
141
|
+
return row;
|
|
142
|
+
}
|
|
143
|
+
function decisionText(set, link) {
|
|
144
|
+
if (set.status === 'waiting') {
|
|
145
|
+
return {
|
|
146
|
+
code: 3,
|
|
147
|
+
data: { changeSet: set },
|
|
148
|
+
text: `… Still waiting for the owner to approve "${set.title}" at ${link}.\n next run app data status again: it waits for the decision`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (set.status === 'applied') {
|
|
152
|
+
const outcomes = set.outcomes ?? [];
|
|
153
|
+
const missed = outcomes
|
|
154
|
+
.map((outcome, index) => ({ outcome, change: set.changes[index] }))
|
|
155
|
+
.filter(({ outcome }) => outcome.status !== 'applied');
|
|
156
|
+
return {
|
|
157
|
+
code: missed.length === 0 ? 0 : 1,
|
|
158
|
+
data: { changeSet: set },
|
|
159
|
+
text: [
|
|
160
|
+
`✓ Approved: ${set.title}. ${outcomes.length - missed.length} of ${outcomes.length} changes are live.`,
|
|
161
|
+
...missed.map(({ outcome, change }) => ` ${outcome.status} ${change?.table ?? ''} ${change?.op ?? ''}: ${outcome.reason ?? ''}`)
|
|
162
|
+
].join('\n')
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const words = {
|
|
166
|
+
declined: 'The owner chose Not yet. Nothing changed on the live site.',
|
|
167
|
+
expired: 'This request expired or a newer one replaced it. Nothing changed.',
|
|
168
|
+
undone: 'The owner approved this, then undid it.'
|
|
169
|
+
}[set.status];
|
|
170
|
+
return { code: 1, data: { changeSet: set }, text: `✗ ${words}` };
|
|
171
|
+
}
|
|
172
|
+
async function waitForDecision(io, name, id) {
|
|
173
|
+
const link = `${DASHBOARD_URL}/sites/${name}/database`;
|
|
174
|
+
const deadline = Date.now() + WAIT_MS;
|
|
175
|
+
for (;;) {
|
|
176
|
+
const set = await signedIn(io, 'app data status', () => callJson(`/api/apps/${name}/changes/${id}`, changeSet));
|
|
177
|
+
if (isOutcome(set))
|
|
178
|
+
return set;
|
|
179
|
+
if (set.status !== 'waiting' || Date.now() > deadline) {
|
|
180
|
+
return decisionText(set, link);
|
|
181
|
+
}
|
|
182
|
+
await sleep(POLL_MS);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function send(io, root, name, asked) {
|
|
186
|
+
const changes = readStaged(root);
|
|
187
|
+
if (changes.length === 0) {
|
|
188
|
+
return refusal('There are no changes to send. Use app data add, update or remove first.');
|
|
189
|
+
}
|
|
190
|
+
if (asked === undefined || asked.trim() === '') {
|
|
191
|
+
return refusal('Say what the owner asked for with --asked "<their words>". They see it beside Approve.', 2);
|
|
192
|
+
}
|
|
193
|
+
const set = await signedIn(io, 'app data send', () => callJson(`/api/apps/${name}/changes`, changeSet, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'content-type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ asked: asked.trim(), changes })
|
|
197
|
+
}));
|
|
198
|
+
if (isOutcome(set))
|
|
199
|
+
return set;
|
|
200
|
+
rmSync(stagedPath(root), { force: true });
|
|
201
|
+
const link = `${DASHBOARD_URL}/sites/${name}/database`;
|
|
202
|
+
io.say(`✓ Sent "${set.title}" to the owner. They also got an email.`);
|
|
203
|
+
io.say(` next tell them: "I've sent the changes to your Pattern dashboard. Open ${link} and press Approve." This waits for their decision.`);
|
|
204
|
+
return waitForDecision(io, name, set.id);
|
|
205
|
+
}
|
|
206
|
+
async function status(io, root, name) {
|
|
207
|
+
const sets = await signedIn(io, 'app data status', () => callJson(`/api/apps/${name}/changes`, z.array(changeSet)));
|
|
208
|
+
if (isOutcome(sets))
|
|
209
|
+
return sets;
|
|
210
|
+
const latest = sets[0];
|
|
211
|
+
const pending = readStaged(root);
|
|
212
|
+
if (latest === undefined) {
|
|
213
|
+
return {
|
|
214
|
+
code: 0,
|
|
215
|
+
data: { staged: pending },
|
|
216
|
+
text: pending.length === 0
|
|
217
|
+
? 'Nothing has been sent for approval yet.'
|
|
218
|
+
: [
|
|
219
|
+
`${pending.length} changes ready to send:`,
|
|
220
|
+
...pendingLines(pending)
|
|
221
|
+
].join('\n')
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return latest.status === 'waiting'
|
|
225
|
+
? waitForDecision(io, name, latest.id)
|
|
226
|
+
: decisionText(latest, `${DASHBOARD_URL}/sites/${name}/database`);
|
|
227
|
+
}
|
|
228
|
+
export async function data(io, root, [action, table, id], options) {
|
|
229
|
+
try {
|
|
230
|
+
const { name } = readProjectFile(root);
|
|
231
|
+
if (action === 'clear') {
|
|
232
|
+
rmSync(stagedPath(root), { force: true });
|
|
233
|
+
return {
|
|
234
|
+
code: 0,
|
|
235
|
+
data: { staged: [] },
|
|
236
|
+
text: '✓ Cleared the changes that were ready to send'
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
sayHost(io);
|
|
240
|
+
if (action === 'tables')
|
|
241
|
+
return await tables(io, name);
|
|
242
|
+
if (action === 'send')
|
|
243
|
+
return await send(io, root, name, valueOf(options, '--asked'));
|
|
244
|
+
if (action === 'status')
|
|
245
|
+
return await status(io, root, name);
|
|
246
|
+
if (table === undefined)
|
|
247
|
+
return refusal(`Say which table\n${DATA_USAGE}`, 2);
|
|
248
|
+
if (action === 'list')
|
|
249
|
+
return await list(io, name, table);
|
|
250
|
+
if (action === 'add') {
|
|
251
|
+
return stage(root, rowsGiven(root, options).map((row) => ({ op: 'add', table, row })));
|
|
252
|
+
}
|
|
253
|
+
if (id === undefined)
|
|
254
|
+
return refusal(`Say which row, by its id\n${DATA_USAGE}`, 2);
|
|
255
|
+
const current = await before(io, name, table, id);
|
|
256
|
+
if (isOutcome(current))
|
|
257
|
+
return current;
|
|
258
|
+
if (action === 'remove') {
|
|
259
|
+
return stage(root, [{ op: 'remove', table, id, before: current }]);
|
|
260
|
+
}
|
|
261
|
+
if (action === 'update') {
|
|
262
|
+
const set = valueOf(options, '--set');
|
|
263
|
+
if (set === undefined)
|
|
264
|
+
return refusal(`Give the new values with --set '{...}'`, 2);
|
|
265
|
+
const after = databaseRow.parse(jsonOf(set, '--set'));
|
|
266
|
+
return stage(root, [{ op: 'update', table, id, before: current, after }]);
|
|
267
|
+
}
|
|
268
|
+
return refusal(`Unknown data command: ${action ?? ''}\n${DATA_USAGE}`, 2);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
return failure(error);
|
|
272
|
+
}
|
|
273
|
+
}
|
package/dist/cli/publishing.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { StarterContent } from '../publishWire.ts';
|
|
2
|
-
type Outcome = {
|
|
3
|
+
export type Outcome = {
|
|
3
4
|
code: number;
|
|
4
5
|
data: Record<string, unknown>;
|
|
5
6
|
text: string;
|
|
@@ -14,10 +15,16 @@ export type PublishingIo = {
|
|
|
14
15
|
build: () => Promise<Outcome>;
|
|
15
16
|
sdkVersion: string;
|
|
16
17
|
};
|
|
18
|
+
export declare function dashboard(): string;
|
|
19
|
+
export declare function sayHost(io: PublishingIo): void;
|
|
20
|
+
export declare function callJson<Value>(path: string, schema: z.ZodType<Value>, init?: RequestInit): Promise<Value>;
|
|
21
|
+
export declare function sleep(ms: number): Promise<unknown>;
|
|
22
|
+
export declare function signedIn<Result>(io: PublishingIo, again: string, work: () => Promise<Result>): Promise<Result | Outcome>;
|
|
23
|
+
export declare function isOutcome(value: unknown): value is Outcome;
|
|
24
|
+
export declare function failure(error: unknown, data?: Record<string, unknown>): Outcome;
|
|
17
25
|
export declare function login(io: PublishingIo): Promise<Outcome>;
|
|
18
26
|
export declare function pull(io: PublishingIo, name: string | undefined, cwd: string, setupCode: string | undefined): Promise<Outcome>;
|
|
19
27
|
export declare function publish(io: PublishingIo, root: string, isStarter: boolean, message: string | undefined, starter: StarterContent | undefined): Promise<Outcome>;
|
|
20
28
|
export declare function addLiveMedia(io: PublishingIo, root: string, form: FormData, again: string): Promise<Outcome>;
|
|
21
29
|
export declare function versions(io: PublishingIo, root: string): Promise<Outcome>;
|
|
22
30
|
export declare function restore(io: PublishingIo, root: string, number: string | undefined): Promise<Outcome>;
|
|
23
|
-
export {};
|
package/dist/cli/publishing.js
CHANGED
|
@@ -48,10 +48,10 @@ const STEP_WORDS = {
|
|
|
48
48
|
'sign-in': 'Turning on sign-in',
|
|
49
49
|
publish: 'Publishing the site'
|
|
50
50
|
};
|
|
51
|
-
function dashboard() {
|
|
51
|
+
export function dashboard() {
|
|
52
52
|
return (process.env['PATTERN_DASHBOARD_URL'] ?? DASHBOARD_URL).replace(/\/$/, '');
|
|
53
53
|
}
|
|
54
|
-
function sayHost(io) {
|
|
54
|
+
export function sayHost(io) {
|
|
55
55
|
io.say(` via ${new URL(dashboard()).host}`);
|
|
56
56
|
}
|
|
57
57
|
const credentials = z.record(z.string(), z.object({ cookie: z.string() }));
|
|
@@ -114,7 +114,7 @@ async function call(path, init = {}) {
|
|
|
114
114
|
}
|
|
115
115
|
return response;
|
|
116
116
|
}
|
|
117
|
-
async function callJson(path, schema, init = {}) {
|
|
117
|
+
export async function callJson(path, schema, init = {}) {
|
|
118
118
|
const response = await call(path, init);
|
|
119
119
|
const body = await response.json().catch(() => null);
|
|
120
120
|
if (!response.ok) {
|
|
@@ -130,7 +130,7 @@ function labelOfThisMachine() {
|
|
|
130
130
|
const name = named.status === 0 ? named.stdout.trim() : '';
|
|
131
131
|
return (name === '' ? 'this computer' : name).slice(0, 80);
|
|
132
132
|
}
|
|
133
|
-
function sleep(ms) {
|
|
133
|
+
export function sleep(ms) {
|
|
134
134
|
return new Promise((done) => setTimeout(done, ms));
|
|
135
135
|
}
|
|
136
136
|
async function waitForApproval(secret) {
|
|
@@ -195,7 +195,7 @@ async function useSetupCode(io, code) {
|
|
|
195
195
|
}
|
|
196
196
|
io.say('✗ That setup code has expired or was already used');
|
|
197
197
|
}
|
|
198
|
-
async function signedIn(io, again, work) {
|
|
198
|
+
export async function signedIn(io, again, work) {
|
|
199
199
|
if (savedCookie() === undefined) {
|
|
200
200
|
const outcome = await signIn(io, again);
|
|
201
201
|
if (outcome.code !== 0)
|
|
@@ -213,10 +213,10 @@ async function signedIn(io, again, work) {
|
|
|
213
213
|
return work();
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
-
function isOutcome(value) {
|
|
216
|
+
export function isOutcome(value) {
|
|
217
217
|
return typeof value === 'object' && value !== null && 'text' in value;
|
|
218
218
|
}
|
|
219
|
-
function failure(error, data = {}) {
|
|
219
|
+
export function failure(error, data = {}) {
|
|
220
220
|
const message = error instanceof TypeError && error.message === 'fetch failed'
|
|
221
221
|
? `Couldn’t reach ${dashboard()}. Check the internet connection and try again.`
|
|
222
222
|
: error instanceof Error
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isNewer(candidate: string, current: string): boolean;
|
|
2
|
+
export declare function latestPattern(fresh?: boolean): Promise<string | undefined>;
|
|
3
|
+
export declare function newerPattern(current: string): Promise<string | undefined>;
|
|
4
|
+
export declare function updateNote(latest: string, current: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const LATEST_URL = 'https://registry.npmjs.org/@trypattern/sdk/latest';
|
|
6
|
+
const CACHE_FILE = join(homedir(), '.config', 'pattern', 'latest-sdk.json');
|
|
7
|
+
const CHECK_MS = 6 * 3_600_000;
|
|
8
|
+
const TIMEOUT_MS = 1500;
|
|
9
|
+
const cached = z.object({ version: z.string(), checkedAt: z.number() });
|
|
10
|
+
const published = z.object({ version: z.string() });
|
|
11
|
+
function partsOf(version) {
|
|
12
|
+
return (version
|
|
13
|
+
.split('-')[0]
|
|
14
|
+
?.split('.')
|
|
15
|
+
.map((part) => Number.parseInt(part, 10) || 0) ?? [0]);
|
|
16
|
+
}
|
|
17
|
+
export function isNewer(candidate, current) {
|
|
18
|
+
const next = partsOf(candidate);
|
|
19
|
+
const now = partsOf(current);
|
|
20
|
+
for (let index = 0; index < Math.max(next.length, now.length); index += 1) {
|
|
21
|
+
const difference = (next[index] ?? 0) - (now[index] ?? 0);
|
|
22
|
+
if (difference !== 0)
|
|
23
|
+
return difference > 0;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function readCache() {
|
|
28
|
+
if (!existsSync(CACHE_FILE))
|
|
29
|
+
return undefined;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = cached.safeParse(JSON.parse(readFileSync(CACHE_FILE, 'utf8')));
|
|
32
|
+
return parsed.success ? parsed.data : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function latestPattern(fresh = false) {
|
|
39
|
+
const remembered = readCache();
|
|
40
|
+
if (!fresh &&
|
|
41
|
+
remembered !== undefined &&
|
|
42
|
+
Date.now() - remembered.checkedAt < CHECK_MS) {
|
|
43
|
+
return remembered.version;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(LATEST_URL, {
|
|
47
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
48
|
+
});
|
|
49
|
+
const body = published.safeParse(await response.json());
|
|
50
|
+
if (!response.ok || !body.success)
|
|
51
|
+
return remembered?.version;
|
|
52
|
+
mkdirSync(dirname(CACHE_FILE), { recursive: true });
|
|
53
|
+
writeFileSync(CACHE_FILE, JSON.stringify({ version: body.data.version, checkedAt: Date.now() }));
|
|
54
|
+
return body.data.version;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return remembered?.version;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function newerPattern(current) {
|
|
61
|
+
if (process.env['PATTERN_NO_UPDATE_CHECK'] !== undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
const latest = await latestPattern();
|
|
64
|
+
return latest !== undefined && isNewer(latest, current) ? latest : undefined;
|
|
65
|
+
}
|
|
66
|
+
export function updateNote(latest, current) {
|
|
67
|
+
return ` update Pattern ${latest} is out (this project has ${current}). Run app update now, then app check. Publish it straight away if nothing else is in progress, otherwise with the next publish, so the live site gets it too. Tell the owner in a few words.`;
|
|
68
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { Placement } from './Placement.ts';
|
|
|
3
3
|
type BadgeProps = {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
variant?: 'primary' | 'tertiary' | 'outline' | 'transparent';
|
|
6
|
-
tone?: 'neutral' | 'brand' | 'positive' | 'danger';
|
|
6
|
+
tone?: 'neutral' | 'brand' | 'positive' | 'warning' | 'danger';
|
|
7
7
|
size?: 'sm' | 'md';
|
|
8
8
|
style?: Placement;
|
|
9
9
|
};
|
package/dist/components/Badge.js
CHANGED
|
@@ -8,6 +8,7 @@ const toneKeys = {
|
|
|
8
8
|
neutral: 'Neutral',
|
|
9
9
|
brand: 'Brand',
|
|
10
10
|
positive: 'Positive',
|
|
11
|
+
warning: 'Warning',
|
|
11
12
|
danger: 'Danger'
|
|
12
13
|
};
|
|
13
14
|
const styles = stylex.create({
|
|
@@ -46,6 +47,10 @@ const looks = stylex.create({
|
|
|
46
47
|
color: colors.onPositiveSolidForeground,
|
|
47
48
|
backgroundColor: colors.positiveSolid
|
|
48
49
|
},
|
|
50
|
+
primaryWarning: {
|
|
51
|
+
color: colors.onWarningSolidForeground,
|
|
52
|
+
backgroundColor: colors.warningSolid
|
|
53
|
+
},
|
|
49
54
|
primaryDanger: {
|
|
50
55
|
color: colors.onDangerSolidForeground,
|
|
51
56
|
backgroundColor: colors.dangerSolid
|
|
@@ -62,6 +67,10 @@ const looks = stylex.create({
|
|
|
62
67
|
color: colors.onPositiveSubtleForeground,
|
|
63
68
|
backgroundColor: colors.positiveSubtle
|
|
64
69
|
},
|
|
70
|
+
tertiaryWarning: {
|
|
71
|
+
color: colors.onWarningSubtleForeground,
|
|
72
|
+
backgroundColor: colors.warningSubtle
|
|
73
|
+
},
|
|
65
74
|
tertiaryDanger: {
|
|
66
75
|
color: colors.dangerText,
|
|
67
76
|
backgroundColor: colors.dangerSubtleHover
|
|
@@ -81,6 +90,11 @@ const looks = stylex.create({
|
|
|
81
90
|
backgroundColor: 'transparent',
|
|
82
91
|
boxShadow: `inset 0 0 0 ${borders.thin} ${colors.positiveBorderDefault}`
|
|
83
92
|
},
|
|
93
|
+
outlineWarning: {
|
|
94
|
+
color: colors.warningForeground,
|
|
95
|
+
backgroundColor: 'transparent',
|
|
96
|
+
boxShadow: `inset 0 0 0 ${borders.thin} ${colors.warningBorderDefault}`
|
|
97
|
+
},
|
|
84
98
|
outlineDanger: {
|
|
85
99
|
color: colors.dangerForeground,
|
|
86
100
|
backgroundColor: 'transparent',
|
|
@@ -98,6 +112,10 @@ const looks = stylex.create({
|
|
|
98
112
|
color: colors.positiveForeground,
|
|
99
113
|
backgroundColor: 'transparent'
|
|
100
114
|
},
|
|
115
|
+
transparentWarning: {
|
|
116
|
+
color: colors.warningForeground,
|
|
117
|
+
backgroundColor: 'transparent'
|
|
118
|
+
},
|
|
101
119
|
transparentDanger: {
|
|
102
120
|
color: colors.dangerForeground,
|
|
103
121
|
backgroundColor: 'transparent'
|
|
@@ -6,7 +6,7 @@ import type { Size } from './Size.ts';
|
|
|
6
6
|
export type ButtonProps = {
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
variant?: 'primary' | 'secondary' | 'tertiary' | 'transparent';
|
|
9
|
-
tone?: 'neutral' | 'brand' | 'danger';
|
|
9
|
+
tone?: 'neutral' | 'brand' | 'positive' | 'danger';
|
|
10
10
|
size?: Size;
|
|
11
11
|
to?: AppPath;
|
|
12
12
|
href?: string;
|