@web-to-figma/desktop 1.0.0
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 +93 -0
- package/assets/chrome-version.json +11 -0
- package/assets/extension/README.md +18 -0
- package/assets/extension/chrome-mv3-headed.crx +0 -0
- package/assets/extension/chrome-mv3-headless.crx +0 -0
- package/bin/web-to-figma.js +5 -0
- package/db/migrations/000001_create_artifact_tables.up.sql +69 -0
- package/db/migrations/000002_create_tag_tables.up.sql +39 -0
- package/db/migrations/000003_seed_tag_values.up.sql +97 -0
- package/dist/auth/captureSession.d.ts +14 -0
- package/dist/auth/captureSession.js +1 -0
- package/dist/auth/jwtSecret.d.ts +2 -0
- package/dist/auth/jwtSecret.js +1 -0
- package/dist/capture/browserPool.d.ts +24 -0
- package/dist/capture/browserPool.js +1 -0
- package/dist/capture/captureService.d.ts +25 -0
- package/dist/capture/captureService.js +1 -0
- package/dist/capture/captureStatus.d.ts +7 -0
- package/dist/capture/captureStatus.js +1 -0
- package/dist/capture/chromeDownloader.d.ts +21 -0
- package/dist/capture/chromeDownloader.js +1 -0
- package/dist/capture/crxUtils.d.ts +4 -0
- package/dist/capture/crxUtils.js +1 -0
- package/dist/capture/extensionPinPolicy.d.ts +10 -0
- package/dist/capture/extensionPinPolicy.js +1 -0
- package/dist/capture/headedBrowserState.d.ts +22 -0
- package/dist/capture/headedBrowserState.js +1 -0
- package/dist/capture/headedExtension.d.ts +7 -0
- package/dist/capture/headedExtension.js +1 -0
- package/dist/capture/manualLaunch.d.ts +2 -0
- package/dist/capture/manualLaunch.js +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +85 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +109 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +241 -0
- package/dist/loadEnv.d.ts +1 -0
- package/dist/loadEnv.js +14 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +32 -0
- package/dist/port.d.ts +4 -0
- package/dist/port.js +19 -0
- package/dist/security/redact.d.ts +3 -0
- package/dist/security/redact.js +34 -0
- package/dist/sentry.d.ts +3 -0
- package/dist/sentry.js +66 -0
- package/dist/server/app.d.ts +18 -0
- package/dist/server/app.js +82 -0
- package/dist/server/checkServer.d.ts +2 -0
- package/dist/server/checkServer.js +24 -0
- package/dist/server/context.d.ts +20 -0
- package/dist/server/context.js +20 -0
- package/dist/server/middleware/desktopJwt.d.ts +10 -0
- package/dist/server/middleware/desktopJwt.js +1 -0
- package/dist/server/routes/capture.d.ts +4 -0
- package/dist/server/routes/capture.js +39 -0
- package/dist/server/routes/captures.d.ts +3 -0
- package/dist/server/routes/captures.js +75 -0
- package/dist/server/routes/collections.d.ts +3 -0
- package/dist/server/routes/collections.js +41 -0
- package/dist/server/routes/files.d.ts +3 -0
- package/dist/server/routes/files.js +18 -0
- package/dist/server/routes/health.d.ts +2 -0
- package/dist/server/routes/health.js +8 -0
- package/dist/server/routes/image.d.ts +2 -0
- package/dist/server/routes/image.js +58 -0
- package/dist/server/routes/proxy.d.ts +2 -0
- package/dist/server/routes/proxy.js +24 -0
- package/dist/server/routes/upload.d.ts +3 -0
- package/dist/server/routes/upload.js +102 -0
- package/dist/server/uploadBody.d.ts +1 -0
- package/dist/server/uploadBody.js +5 -0
- package/dist/storage/captureRepo.d.ts +21 -0
- package/dist/storage/captureRepo.js +172 -0
- package/dist/storage/collectionRepo.d.ts +14 -0
- package/dist/storage/collectionRepo.js +104 -0
- package/dist/storage/db.d.ts +10 -0
- package/dist/storage/db.js +30 -0
- package/dist/storage/fileStore.d.ts +26 -0
- package/dist/storage/fileStore.js +93 -0
- package/dist/storage/migrate.d.ts +8 -0
- package/dist/storage/migrate.js +61 -0
- package/dist/storage/seeds.d.ts +7 -0
- package/dist/storage/seeds.js +30 -0
- package/dist/storage/tagRepo.d.ts +7 -0
- package/dist/storage/tagRepo.js +31 -0
- package/dist/terminal/banner.d.ts +10 -0
- package/dist/terminal/banner.js +115 -0
- package/dist/terminal/chromeInstall.d.ts +3 -0
- package/dist/terminal/chromeInstall.js +45 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +1 -0
- package/dist/updater/installer.d.ts +2 -0
- package/dist/updater/installer.js +61 -0
- package/dist/updater/runtime.d.ts +25 -0
- package/dist/updater/runtime.js +153 -0
- package/dist/updater/types.d.ts +29 -0
- package/dist/updater/types.js +1 -0
- package/dist/updater/updateLog.d.ts +3 -0
- package/dist/updater/updateLog.js +7 -0
- package/dist/updater/versionCheck.d.ts +5 -0
- package/dist/updater/versionCheck.js +59 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +55 -0
- package/package.json +72 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { LOCAL_USER_ID, nowIso } from '../utils.js';
|
|
2
|
+
export class CollectionRepo {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
saveCollection(collection) {
|
|
8
|
+
const now = nowIso();
|
|
9
|
+
this.db
|
|
10
|
+
.prepare(`
|
|
11
|
+
INSERT INTO collection (
|
|
12
|
+
created_at, updated_at, name, external_id, parent_external_id,
|
|
13
|
+
active, is_default, user_id, icon_url
|
|
14
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
15
|
+
ON CONFLICT(user_id, external_id) DO UPDATE SET
|
|
16
|
+
name = excluded.name,
|
|
17
|
+
parent_external_id = excluded.parent_external_id,
|
|
18
|
+
active = excluded.active,
|
|
19
|
+
updated_at = excluded.updated_at
|
|
20
|
+
`)
|
|
21
|
+
.run(now, now, collection.name, collection.externalId, collection.parentExternalId ?? null, collection.active ? 1 : 0, collection.isDefault ? 1 : 0, collection.userId ?? LOCAL_USER_ID, collection.iconUrl ?? null);
|
|
22
|
+
}
|
|
23
|
+
saveCollections(collections) {
|
|
24
|
+
for (const collection of collections) {
|
|
25
|
+
this.saveCollection(collection);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
listCollections() {
|
|
29
|
+
const rows = this.db
|
|
30
|
+
.prepare(`
|
|
31
|
+
SELECT id, external_id, user_id, name, parent_external_id, active, is_default, icon_url
|
|
32
|
+
FROM collection
|
|
33
|
+
WHERE user_id = ? AND active = 1 AND deleted_at IS NULL
|
|
34
|
+
ORDER BY updated_at DESC
|
|
35
|
+
`)
|
|
36
|
+
.all(LOCAL_USER_ID);
|
|
37
|
+
return rows.map(rowToCollection);
|
|
38
|
+
}
|
|
39
|
+
listCollectionsWithMetadata() {
|
|
40
|
+
const rows = this.db
|
|
41
|
+
.prepare(`
|
|
42
|
+
SELECT c.id, c.external_id, c.user_id, c.name, c.parent_external_id,
|
|
43
|
+
c.active, c.is_default, c.icon_url,
|
|
44
|
+
COALESCE(m.capture_count, 0) as capture_count
|
|
45
|
+
FROM collection c
|
|
46
|
+
LEFT JOIN (
|
|
47
|
+
SELECT collection_external_id, COUNT(*) as capture_count
|
|
48
|
+
FROM capture
|
|
49
|
+
WHERE user_id = ? AND deleted_at IS NULL
|
|
50
|
+
GROUP BY collection_external_id
|
|
51
|
+
) m ON c.external_id = m.collection_external_id
|
|
52
|
+
WHERE c.user_id = ? AND c.active = 1 AND c.deleted_at IS NULL
|
|
53
|
+
ORDER BY c.updated_at DESC
|
|
54
|
+
`)
|
|
55
|
+
.all(LOCAL_USER_ID, LOCAL_USER_ID);
|
|
56
|
+
return rows.map((row) => ({
|
|
57
|
+
...rowToCollection(row),
|
|
58
|
+
captureCount: Number(row.capture_count),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
getCollectionOwner(externalId) {
|
|
62
|
+
const row = this.db
|
|
63
|
+
.prepare('SELECT user_id FROM collection WHERE external_id = ? AND deleted_at IS NULL')
|
|
64
|
+
.get(externalId);
|
|
65
|
+
return row?.user_id ?? null;
|
|
66
|
+
}
|
|
67
|
+
getDefaultCollectionExternalId() {
|
|
68
|
+
const row = this.db
|
|
69
|
+
.prepare('SELECT external_id FROM collection WHERE user_id = ? AND is_default = 1 AND active = 1 AND deleted_at IS NULL')
|
|
70
|
+
.get(LOCAL_USER_ID);
|
|
71
|
+
if (!row) {
|
|
72
|
+
throw new Error('Default collection not found');
|
|
73
|
+
}
|
|
74
|
+
return row.external_id;
|
|
75
|
+
}
|
|
76
|
+
deleteCollection(collectionExternalId, replacementCollectionId) {
|
|
77
|
+
const tx = this.db.transaction(() => {
|
|
78
|
+
this.db
|
|
79
|
+
.prepare('UPDATE capture SET collection_external_id = ?, updated_at = ? WHERE collection_external_id = ?')
|
|
80
|
+
.run(replacementCollectionId, nowIso(), collectionExternalId);
|
|
81
|
+
this.db
|
|
82
|
+
.prepare('UPDATE collection SET active = 0, updated_at = ? WHERE external_id = ?')
|
|
83
|
+
.run(nowIso(), collectionExternalId);
|
|
84
|
+
});
|
|
85
|
+
tx();
|
|
86
|
+
}
|
|
87
|
+
updateIcon(externalId, iconUrl) {
|
|
88
|
+
this.db
|
|
89
|
+
.prepare('UPDATE collection SET icon_url = ?, updated_at = ? WHERE external_id = ?')
|
|
90
|
+
.run(iconUrl, nowIso(), externalId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function rowToCollection(row) {
|
|
94
|
+
return {
|
|
95
|
+
id: row.id,
|
|
96
|
+
externalId: row.external_id,
|
|
97
|
+
userId: row.user_id,
|
|
98
|
+
name: row.name,
|
|
99
|
+
parentExternalId: row.parent_external_id ?? null,
|
|
100
|
+
active: Boolean(row.active),
|
|
101
|
+
isDefault: Boolean(row.is_default),
|
|
102
|
+
iconUrl: row.icon_url ?? null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import { runMigrations } from './migrate.js';
|
|
3
|
+
export declare function initDb(dbPath: string, logger?: Parameters<typeof runMigrations>[1]): Database.Database;
|
|
4
|
+
export declare function getDb(): Database.Database;
|
|
5
|
+
export declare function closeDb(): void;
|
|
6
|
+
export declare function getDbDiagnostics(db: Database.Database): {
|
|
7
|
+
schemaVersion: number;
|
|
8
|
+
journalMode: string;
|
|
9
|
+
foreignKeys: number;
|
|
10
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runMigrations } from './migrate.js';
|
|
2
|
+
import { seedDefaultsIfNeeded } from './seeds.js';
|
|
3
|
+
let dbInstance = null;
|
|
4
|
+
export function initDb(dbPath, logger) {
|
|
5
|
+
if (dbInstance) {
|
|
6
|
+
return dbInstance;
|
|
7
|
+
}
|
|
8
|
+
dbInstance = runMigrations(dbPath, logger);
|
|
9
|
+
seedDefaultsIfNeeded(dbInstance);
|
|
10
|
+
return dbInstance;
|
|
11
|
+
}
|
|
12
|
+
export function getDb() {
|
|
13
|
+
if (!dbInstance) {
|
|
14
|
+
throw new Error('Database not initialized');
|
|
15
|
+
}
|
|
16
|
+
return dbInstance;
|
|
17
|
+
}
|
|
18
|
+
export function closeDb() {
|
|
19
|
+
if (dbInstance) {
|
|
20
|
+
dbInstance.close();
|
|
21
|
+
dbInstance = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function getDbDiagnostics(db) {
|
|
25
|
+
const schemaVersion = db.prepare('SELECT MAX(version) as version FROM schema_migration').get()
|
|
26
|
+
?.version ?? 0;
|
|
27
|
+
const journalMode = db.pragma('journal_mode', { simple: true }) ?? 'unknown';
|
|
28
|
+
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
29
|
+
return { schemaVersion, journalMode, foreignKeys };
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResolvedPaths } from '../config.js';
|
|
2
|
+
export declare const MULTIPART_UPLOAD_TTL_MS: number;
|
|
3
|
+
export declare class FileStore {
|
|
4
|
+
private readonly paths;
|
|
5
|
+
constructor(paths: ResolvedPaths);
|
|
6
|
+
resolveKeyPath(key: string): string;
|
|
7
|
+
writeFile(key: string, data: Buffer, _contentType?: string, _contentEncoding?: string | null): void;
|
|
8
|
+
readFile(relativePath: string): {
|
|
9
|
+
data: Buffer;
|
|
10
|
+
filePath: string;
|
|
11
|
+
};
|
|
12
|
+
partPath(uploadId: string, partNumber: number): string;
|
|
13
|
+
writePart(uploadId: string, partNumber: number, data: Buffer): void;
|
|
14
|
+
completeMultipart(key: string, uploadId: string, numParts: number): void;
|
|
15
|
+
accessUrl(baseUrl: string, key: string): string;
|
|
16
|
+
}
|
|
17
|
+
export type MultipartUploadState = {
|
|
18
|
+
uploadId: string;
|
|
19
|
+
key: string;
|
|
20
|
+
numParts: number;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function registerMultipartUpload(state: MultipartUploadState): void;
|
|
24
|
+
export declare function getMultipartUpload(uploadId: string): MultipartUploadState | undefined;
|
|
25
|
+
export declare function removeMultipartUpload(uploadId: string): void;
|
|
26
|
+
export declare function cleanupStaleMultipartUploads(tmpDir: string, now?: number): number;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { isValidArtifactKey } from '../utils.js';
|
|
4
|
+
const MAX_FILE_SIZE = 500 * 1024 * 1024;
|
|
5
|
+
export const MULTIPART_UPLOAD_TTL_MS = 2 * 60 * 60 * 1000;
|
|
6
|
+
export class FileStore {
|
|
7
|
+
paths;
|
|
8
|
+
constructor(paths) {
|
|
9
|
+
this.paths = paths;
|
|
10
|
+
}
|
|
11
|
+
resolveKeyPath(key) {
|
|
12
|
+
if (!isValidArtifactKey(key)) {
|
|
13
|
+
throw new Error('Invalid artifact key');
|
|
14
|
+
}
|
|
15
|
+
const resolved = path.resolve(this.paths.artifactsDir, key);
|
|
16
|
+
if (!resolved.startsWith(path.resolve(this.paths.artifactsDir))) {
|
|
17
|
+
throw new Error('Path traversal blocked');
|
|
18
|
+
}
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
writeFile(key, data, _contentType, _contentEncoding) {
|
|
22
|
+
if (data.byteLength > MAX_FILE_SIZE) {
|
|
23
|
+
throw new Error('File exceeds maximum size');
|
|
24
|
+
}
|
|
25
|
+
const filePath = this.resolveKeyPath(key);
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(filePath, data);
|
|
28
|
+
}
|
|
29
|
+
readFile(relativePath) {
|
|
30
|
+
const normalized = relativePath.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
31
|
+
const filePath = path.resolve(this.paths.artifactsDir, normalized);
|
|
32
|
+
if (!filePath.startsWith(path.resolve(this.paths.artifactsDir))) {
|
|
33
|
+
throw new Error('Path traversal blocked');
|
|
34
|
+
}
|
|
35
|
+
if (!fs.existsSync(filePath)) {
|
|
36
|
+
throw new Error('File not found');
|
|
37
|
+
}
|
|
38
|
+
return { data: fs.readFileSync(filePath), filePath };
|
|
39
|
+
}
|
|
40
|
+
partPath(uploadId, partNumber) {
|
|
41
|
+
return path.join(this.paths.tmpDir, uploadId, `part-${partNumber}`);
|
|
42
|
+
}
|
|
43
|
+
writePart(uploadId, partNumber, data) {
|
|
44
|
+
if (data.byteLength > MAX_FILE_SIZE) {
|
|
45
|
+
throw new Error('Part exceeds maximum size');
|
|
46
|
+
}
|
|
47
|
+
const partFile = this.partPath(uploadId, partNumber);
|
|
48
|
+
fs.mkdirSync(path.dirname(partFile), { recursive: true });
|
|
49
|
+
fs.writeFileSync(partFile, data);
|
|
50
|
+
}
|
|
51
|
+
completeMultipart(key, uploadId, numParts) {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let totalBytes = 0;
|
|
54
|
+
for (let i = 1; i <= numParts; i++) {
|
|
55
|
+
const partFile = this.partPath(uploadId, i);
|
|
56
|
+
if (!fs.existsSync(partFile)) {
|
|
57
|
+
throw new Error(`Missing part ${i}`);
|
|
58
|
+
}
|
|
59
|
+
const partData = fs.readFileSync(partFile);
|
|
60
|
+
totalBytes += partData.byteLength;
|
|
61
|
+
if (totalBytes > MAX_FILE_SIZE) {
|
|
62
|
+
throw new Error('Multipart upload exceeds maximum size');
|
|
63
|
+
}
|
|
64
|
+
chunks.push(partData);
|
|
65
|
+
}
|
|
66
|
+
this.writeFile(key, Buffer.concat(chunks));
|
|
67
|
+
fs.rmSync(path.join(this.paths.tmpDir, uploadId), { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
accessUrl(baseUrl, key) {
|
|
70
|
+
return `${baseUrl}/files/${key}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const multipartUploads = new Map();
|
|
74
|
+
export function registerMultipartUpload(state) {
|
|
75
|
+
multipartUploads.set(state.uploadId, state);
|
|
76
|
+
}
|
|
77
|
+
export function getMultipartUpload(uploadId) {
|
|
78
|
+
return multipartUploads.get(uploadId);
|
|
79
|
+
}
|
|
80
|
+
export function removeMultipartUpload(uploadId) {
|
|
81
|
+
multipartUploads.delete(uploadId);
|
|
82
|
+
}
|
|
83
|
+
export function cleanupStaleMultipartUploads(tmpDir, now = Date.now()) {
|
|
84
|
+
let removed = 0;
|
|
85
|
+
for (const [uploadId, upload] of multipartUploads.entries()) {
|
|
86
|
+
if (now - upload.createdAt > MULTIPART_UPLOAD_TTL_MS) {
|
|
87
|
+
multipartUploads.delete(uploadId);
|
|
88
|
+
fs.rmSync(path.join(tmpDir, uploadId), { recursive: true, force: true });
|
|
89
|
+
removed += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return removed;
|
|
93
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
export type MigrationLogger = {
|
|
3
|
+
info: (obj: Record<string, unknown>, msg: string) => void;
|
|
4
|
+
error: (obj: Record<string, unknown>, msg: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function getLatestMigrationVersion(migrationsDir?: string): number;
|
|
7
|
+
export declare function getAppliedSchemaVersion(db: Database.Database): number;
|
|
8
|
+
export declare function runMigrations(dbPath: string, logger?: MigrationLogger): Database.Database;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { bundledMigrationsDir } from '../config.js';
|
|
5
|
+
import { nowIso } from '../utils.js';
|
|
6
|
+
const defaultLogger = {
|
|
7
|
+
info: (obj, msg) => console.log(msg, obj),
|
|
8
|
+
error: (obj, msg) => console.error(msg, obj),
|
|
9
|
+
};
|
|
10
|
+
export function getLatestMigrationVersion(migrationsDir = bundledMigrationsDir()) {
|
|
11
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.up.sql'));
|
|
15
|
+
if (files.length === 0) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
return Math.max(...files.map((f) => parseInt(f.split('_')[0], 10)));
|
|
19
|
+
}
|
|
20
|
+
export function getAppliedSchemaVersion(db) {
|
|
21
|
+
const row = db.prepare('SELECT MAX(version) as version FROM schema_migration').get();
|
|
22
|
+
return row?.version ?? 0;
|
|
23
|
+
}
|
|
24
|
+
export function runMigrations(dbPath, logger = defaultLogger) {
|
|
25
|
+
const db = new Database(dbPath);
|
|
26
|
+
db.pragma('journal_mode = WAL');
|
|
27
|
+
db.pragma('foreign_keys = ON');
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS schema_migration (
|
|
30
|
+
version INTEGER PRIMARY KEY NOT NULL,
|
|
31
|
+
applied_at TEXT NOT NULL
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
const applied = new Set(db.prepare('SELECT version FROM schema_migration').all().map((r) => r.version));
|
|
35
|
+
const migrationsDir = bundledMigrationsDir();
|
|
36
|
+
const files = fs
|
|
37
|
+
.readdirSync(migrationsDir)
|
|
38
|
+
.filter((f) => f.endsWith('.up.sql'))
|
|
39
|
+
.sort();
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const version = parseInt(file.split('_')[0], 10);
|
|
42
|
+
if (applied.has(version)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
|
|
46
|
+
const migrate = db.transaction(() => {
|
|
47
|
+
db.exec(sql);
|
|
48
|
+
db.prepare('INSERT INTO schema_migration (version, applied_at) VALUES (?, ?)').run(version, nowIso());
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
migrate();
|
|
52
|
+
logger.info({ version, file }, 'migration_applied');
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logger.error({ version, file, err }, 'migration_failed');
|
|
56
|
+
db.close();
|
|
57
|
+
throw new Error(`Database migration ${file} failed. See desktop.log.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return db;
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { LOCAL_USER_ID, nowIso } from '../utils.js';
|
|
3
|
+
const DEFAULT_COLLECTIONS = [
|
|
4
|
+
{ name: 'Miscellaneous', isDefault: true },
|
|
5
|
+
{ name: 'Landing Page', isDefault: false },
|
|
6
|
+
{ name: 'Dashboard', isDefault: false },
|
|
7
|
+
{ name: 'Illustrations', isDefault: false },
|
|
8
|
+
{ name: 'Headers', isDefault: false },
|
|
9
|
+
{ name: 'Cards', isDefault: false },
|
|
10
|
+
{ name: 'Buttons', isDefault: false },
|
|
11
|
+
];
|
|
12
|
+
export function seedDefaultsIfNeeded(db) {
|
|
13
|
+
const row = db.prepare('SELECT COUNT(*) as n FROM collection').get();
|
|
14
|
+
if (row.n > 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const insert = db.prepare(`
|
|
18
|
+
INSERT INTO collection (
|
|
19
|
+
created_at, updated_at, name, external_id, active, is_default, user_id
|
|
20
|
+
) VALUES (?, ?, ?, ?, 1, ?, ?)
|
|
21
|
+
`);
|
|
22
|
+
const now = nowIso();
|
|
23
|
+
const seed = db.transaction(() => {
|
|
24
|
+
for (const collection of DEFAULT_COLLECTIONS) {
|
|
25
|
+
insert.run(now, now, collection.name, uuidv4(), collection.isDefault ? 1 : 0, LOCAL_USER_ID);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
seed();
|
|
29
|
+
}
|
|
30
|
+
export { DEFAULT_COLLECTIONS };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class TagRepo {
|
|
2
|
+
db;
|
|
3
|
+
constructor(db) {
|
|
4
|
+
this.db = db;
|
|
5
|
+
}
|
|
6
|
+
getAllTags() {
|
|
7
|
+
const tagTypes = this.db
|
|
8
|
+
.prepare('SELECT id, name FROM tag_type WHERE active = 1 AND deleted_at IS NULL')
|
|
9
|
+
.all();
|
|
10
|
+
const tagValues = this.db
|
|
11
|
+
.prepare('SELECT id, tag_type_id, name, active FROM tag_value WHERE active = 1 AND deleted_at IS NULL')
|
|
12
|
+
.all();
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const tagType of tagTypes) {
|
|
15
|
+
result[tagType.name] = [];
|
|
16
|
+
}
|
|
17
|
+
for (const value of tagValues) {
|
|
18
|
+
const tagType = tagTypes.find((t) => t.id === value.tag_type_id);
|
|
19
|
+
if (!tagType) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
result[tagType.name].push({
|
|
23
|
+
id: value.id,
|
|
24
|
+
tagTypeId: value.tag_type_id,
|
|
25
|
+
name: value.name,
|
|
26
|
+
active: Boolean(value.active),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ServerBannerOptions = {
|
|
2
|
+
currentVersion: string;
|
|
3
|
+
port: number;
|
|
4
|
+
updateAvailable: {
|
|
5
|
+
latest: string;
|
|
6
|
+
} | null;
|
|
7
|
+
updateKey: string;
|
|
8
|
+
showNpxUpdateHint: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function formatServerBanner(options: ServerBannerOptions): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const BOX_WIDTH = 59;
|
|
2
|
+
const BRAND_PURPLE = '38;2;130;67;234';
|
|
3
|
+
const BRAND_PURPLE_BRIGHT = '38;2;112;25;255';
|
|
4
|
+
const ANSI = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
purple: '\x1b[35m',
|
|
9
|
+
brightPurple: '\x1b[95m',
|
|
10
|
+
cyan: '\x1b[36m',
|
|
11
|
+
brightCyan: '\x1b[96m',
|
|
12
|
+
green: '\x1b[32m',
|
|
13
|
+
brightGreen: '\x1b[92m',
|
|
14
|
+
yellow: '\x1b[33m',
|
|
15
|
+
brightYellow: '\x1b[93m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
brightRed: '\x1b[91m',
|
|
18
|
+
white: '\x1b[37m',
|
|
19
|
+
brightWhite: '\x1b[97m',
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
|
+
};
|
|
22
|
+
function useColor() {
|
|
23
|
+
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
24
|
+
}
|
|
25
|
+
function visibleLength(text) {
|
|
26
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
27
|
+
}
|
|
28
|
+
function c(code, text, color) {
|
|
29
|
+
return color ? `${code}${text}${ANSI.reset}` : text;
|
|
30
|
+
}
|
|
31
|
+
function brand(text, color, bright = false) {
|
|
32
|
+
if (!color) {
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
const rgb = bright ? BRAND_PURPLE_BRIGHT : BRAND_PURPLE;
|
|
36
|
+
return `\x1b[1;${rgb}m${text}${ANSI.reset}`;
|
|
37
|
+
}
|
|
38
|
+
function brandBorder(text, color) {
|
|
39
|
+
if (!color) {
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
return `\x1b[${BRAND_PURPLE}m${text}${ANSI.reset}`;
|
|
43
|
+
}
|
|
44
|
+
function row(content, color) {
|
|
45
|
+
const inner = ` ${content}${' '.repeat(Math.max(0, BOX_WIDTH - visibleLength(content)))} `;
|
|
46
|
+
if (color) {
|
|
47
|
+
return `${brandBorder('║', color)}${inner}${brandBorder('║', color)}`;
|
|
48
|
+
}
|
|
49
|
+
return `║${inner}║`;
|
|
50
|
+
}
|
|
51
|
+
function topBorder(color) {
|
|
52
|
+
const line = '═'.repeat(BOX_WIDTH + 2);
|
|
53
|
+
return color ? `${brandBorder(`╔${line}╗`, color)}` : `╔${line}╗`;
|
|
54
|
+
}
|
|
55
|
+
function bottomBorder(color) {
|
|
56
|
+
const line = '═'.repeat(BOX_WIDTH + 2);
|
|
57
|
+
return color ? `${brandBorder(`╚${line}╝`, color)}` : `╚${line}╝`;
|
|
58
|
+
}
|
|
59
|
+
function emptyRow(color) {
|
|
60
|
+
return row('', color);
|
|
61
|
+
}
|
|
62
|
+
export function formatServerBanner(options) {
|
|
63
|
+
const { currentVersion, updateAvailable, updateKey, showNpxUpdateHint } = options;
|
|
64
|
+
const color = useColor();
|
|
65
|
+
const title = [
|
|
66
|
+
brand('Web to Figma', color),
|
|
67
|
+
c(ANSI.dim + ANSI.gray, ` v${currentVersion}`, color),
|
|
68
|
+
].join('');
|
|
69
|
+
const statusLine = [
|
|
70
|
+
c(ANSI.brightGreen, '●', color),
|
|
71
|
+
' ',
|
|
72
|
+
c(ANSI.dim + ANSI.gray, 'Ready — keep this window open', color),
|
|
73
|
+
].join('');
|
|
74
|
+
const stopLine = [
|
|
75
|
+
c(ANSI.dim + ANSI.gray, 'Press ', color),
|
|
76
|
+
c(ANSI.bold + ANSI.brightRed, 'Ctrl+C', color),
|
|
77
|
+
c(ANSI.dim + ANSI.gray, ' to stop', color),
|
|
78
|
+
].join('');
|
|
79
|
+
const lines = [
|
|
80
|
+
topBorder(color),
|
|
81
|
+
emptyRow(color),
|
|
82
|
+
row(title, color),
|
|
83
|
+
emptyRow(color),
|
|
84
|
+
row(statusLine, color),
|
|
85
|
+
];
|
|
86
|
+
if (updateAvailable) {
|
|
87
|
+
const updateTitle = [
|
|
88
|
+
c(ANSI.bold + ANSI.brightYellow, '⬆', color),
|
|
89
|
+
' ',
|
|
90
|
+
c(ANSI.bold + ANSI.brightYellow, 'Update available:', color),
|
|
91
|
+
' ',
|
|
92
|
+
c(ANSI.bold + ANSI.brightGreen, `v${updateAvailable.latest}`, color),
|
|
93
|
+
].join('');
|
|
94
|
+
const updateAction = [
|
|
95
|
+
c(ANSI.dim + ANSI.gray, 'Press ', color),
|
|
96
|
+
c(ANSI.bold + ANSI.brightYellow, updateKey, color),
|
|
97
|
+
c(ANSI.dim + ANSI.gray, ' to update now', color),
|
|
98
|
+
].join('');
|
|
99
|
+
lines.push(emptyRow(color));
|
|
100
|
+
lines.push(row(updateTitle, color));
|
|
101
|
+
lines.push(row(updateAction, color));
|
|
102
|
+
if (showNpxUpdateHint) {
|
|
103
|
+
const npxLine = [
|
|
104
|
+
c(ANSI.dim + ANSI.gray, 'Or run: ', color),
|
|
105
|
+
brand('npx @web-to-figma/desktop@latest start', color, true),
|
|
106
|
+
].join('');
|
|
107
|
+
lines.push(row(npxLine, color));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
lines.push(emptyRow(color));
|
|
111
|
+
lines.push(row(stopLine, color));
|
|
112
|
+
lines.push(emptyRow(color));
|
|
113
|
+
lines.push(bottomBorder(color));
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
const CHROME_INSTALL_TEXT = 'Web to Figma requires its own Chrome browser to run. Downloading it.';
|
|
3
|
+
let activeSpinner = null;
|
|
4
|
+
function useSpinner() {
|
|
5
|
+
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
6
|
+
}
|
|
7
|
+
export function startChromeInstallSpinner() {
|
|
8
|
+
if (activeSpinner) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (useSpinner()) {
|
|
12
|
+
activeSpinner = ora({
|
|
13
|
+
text: CHROME_INSTALL_TEXT,
|
|
14
|
+
color: 'magenta',
|
|
15
|
+
}).start();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(CHROME_INSTALL_TEXT);
|
|
19
|
+
}
|
|
20
|
+
export function stopChromeInstallSpinner(outcome) {
|
|
21
|
+
if (!activeSpinner) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (outcome === 'success') {
|
|
25
|
+
activeSpinner.succeed('Chrome browser ready');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
activeSpinner.stop();
|
|
29
|
+
}
|
|
30
|
+
activeSpinner = null;
|
|
31
|
+
}
|
|
32
|
+
export function printChromeInstallError(error, logPath, verbose = false) {
|
|
33
|
+
stopChromeInstallSpinner('stopped');
|
|
34
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
35
|
+
console.error('');
|
|
36
|
+
console.error('Web to Figma could not download its Chrome browser.');
|
|
37
|
+
console.error(message);
|
|
38
|
+
console.error(`See ${logPath} for details.`);
|
|
39
|
+
console.error('Tips: check your network connection and corporate proxy or firewall settings.');
|
|
40
|
+
console.error('Retry with: npx @web-to-figma/desktop doctor --update-chrome');
|
|
41
|
+
if (verbose && error instanceof Error && error.stack) {
|
|
42
|
+
console.error(error.stack);
|
|
43
|
+
}
|
|
44
|
+
console.error('');
|
|
45
|
+
}
|