workplane 0.4.1 → 0.4.2
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 +31 -1
- package/dist/cli.js +164 -8
- package/dist/cli.js.map +1 -1
- package/dist/migrate.js +243 -9
- package/dist/migrate.js.map +1 -1
- package/dist/migrations/pg/0001_loud_otto_octavius.sql +45 -0
- package/dist/migrations/pg/meta/0001_snapshot.json +900 -0
- package/dist/migrations/pg/meta/_journal.json +7 -0
- package/dist/migrations/sqlite/0001_curvy_anita_blake.sql +45 -0
- package/dist/migrations/sqlite/meta/0001_snapshot.json +841 -0
- package/dist/migrations/sqlite/meta/_journal.json +7 -0
- package/dist/server.js +1194 -62
- package/dist/server.js.map +1 -1
- package/dist/setup.js +252 -12
- package/dist/setup.js.map +1 -1
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -17,11 +17,14 @@ __export(pg_exports, {
|
|
|
17
17
|
runInputEvents: () => runInputEvents,
|
|
18
18
|
runLogs: () => runLogs,
|
|
19
19
|
runs: () => runs,
|
|
20
|
-
tasks: () => tasks
|
|
20
|
+
tasks: () => tasks,
|
|
21
|
+
workplanRuns: () => workplanRuns,
|
|
22
|
+
workplanSchedules: () => workplanSchedules,
|
|
23
|
+
workplanStepResults: () => workplanStepResults
|
|
21
24
|
});
|
|
22
25
|
import { isNull, sql } from "drizzle-orm";
|
|
23
|
-
import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
24
|
-
var tasks, nodes, runs, runLogs, artifacts, runInputEvents;
|
|
26
|
+
import { bigint, bigserial, boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
27
|
+
var tasks, nodes, runs, runLogs, artifacts, workplanSchedules, workplanRuns, workplanStepResults, runInputEvents;
|
|
25
28
|
var init_pg = __esm({
|
|
26
29
|
"../db/src/schema/pg.ts"() {
|
|
27
30
|
"use strict";
|
|
@@ -82,6 +85,49 @@ var init_pg = __esm({
|
|
|
82
85
|
}, (table) => ({
|
|
83
86
|
runIdIdx: index("idx_artifacts_run_id").on(table.runId)
|
|
84
87
|
}));
|
|
88
|
+
workplanSchedules = pgTable("workplan_schedules", {
|
|
89
|
+
id: text("id").primaryKey(),
|
|
90
|
+
planId: text("plan_id").notNull(),
|
|
91
|
+
name: text("name").notNull(),
|
|
92
|
+
cronExpression: text("cron_expression").notNull(),
|
|
93
|
+
timezone: text("timezone").notNull(),
|
|
94
|
+
inputs: jsonb("inputs").$type().notNull().default({}),
|
|
95
|
+
enabled: boolean("enabled").notNull().default(true),
|
|
96
|
+
lastRunAt: timestamp("last_run_at", { withTimezone: true }),
|
|
97
|
+
nextRunAt: timestamp("next_run_at", { withTimezone: true }),
|
|
98
|
+
createdBy: text("created_by"),
|
|
99
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
100
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
101
|
+
}, (table) => ({
|
|
102
|
+
dueIdx: index("idx_workplan_schedules_due").on(table.nextRunAt)
|
|
103
|
+
}));
|
|
104
|
+
workplanRuns = pgTable("workplan_runs", {
|
|
105
|
+
id: text("id").primaryKey(),
|
|
106
|
+
scheduleId: text("schedule_id").references(() => workplanSchedules.id),
|
|
107
|
+
planId: text("plan_id").notNull(),
|
|
108
|
+
planName: text("plan_name").notNull(),
|
|
109
|
+
status: text("status").notNull().default("running"),
|
|
110
|
+
idempotencyKey: text("idempotency_key"),
|
|
111
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
112
|
+
endedAt: timestamp("ended_at", { withTimezone: true }),
|
|
113
|
+
error: text("error")
|
|
114
|
+
}, (table) => ({
|
|
115
|
+
scheduleIdx: index("idx_workplan_runs_schedule_id").on(table.scheduleId),
|
|
116
|
+
idempotencyUnique: uniqueIndex("idx_workplan_runs_idempotency").on(table.idempotencyKey)
|
|
117
|
+
}));
|
|
118
|
+
workplanStepResults = pgTable("workplan_step_results", {
|
|
119
|
+
id: text("id").primaryKey(),
|
|
120
|
+
workplanRunId: text("workplan_run_id").notNull().references(() => workplanRuns.id),
|
|
121
|
+
stepId: text("step_id").notNull(),
|
|
122
|
+
stepName: text("step_name").notNull(),
|
|
123
|
+
output: text("output"),
|
|
124
|
+
exitCode: integer("exit_code"),
|
|
125
|
+
durationMs: integer("duration_ms"),
|
|
126
|
+
metadata: jsonb("metadata").$type(),
|
|
127
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
128
|
+
}, (table) => ({
|
|
129
|
+
runIdx: index("idx_workplan_step_results_run_id").on(table.workplanRunId)
|
|
130
|
+
}));
|
|
85
131
|
runInputEvents = pgTable("run_input_events", {
|
|
86
132
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
87
133
|
runId: text("run_id").notNull().references(() => runs.id),
|
|
@@ -98,7 +144,7 @@ var init_pg = __esm({
|
|
|
98
144
|
});
|
|
99
145
|
|
|
100
146
|
// ../db/src/pg-store.ts
|
|
101
|
-
import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
|
|
147
|
+
import { and, arrayContains, asc, desc, eq, gt, isNotNull, lte, sql as sql2 } from "drizzle-orm";
|
|
102
148
|
var init_pg_store = __esm({
|
|
103
149
|
"../db/src/pg-store.ts"() {
|
|
104
150
|
"use strict";
|
|
@@ -114,11 +160,14 @@ __export(sqlite_exports, {
|
|
|
114
160
|
runInputEvents: () => runInputEvents2,
|
|
115
161
|
runLogs: () => runLogs2,
|
|
116
162
|
runs: () => runs2,
|
|
117
|
-
tasks: () => tasks2
|
|
163
|
+
tasks: () => tasks2,
|
|
164
|
+
workplanRuns: () => workplanRuns2,
|
|
165
|
+
workplanSchedules: () => workplanSchedules2,
|
|
166
|
+
workplanStepResults: () => workplanStepResults2
|
|
118
167
|
});
|
|
119
168
|
import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
|
|
120
169
|
import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
|
|
121
|
-
var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, runInputEvents2;
|
|
170
|
+
var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, workplanSchedules2, workplanRuns2, workplanStepResults2, runInputEvents2;
|
|
122
171
|
var init_sqlite = __esm({
|
|
123
172
|
"../db/src/schema/sqlite.ts"() {
|
|
124
173
|
"use strict";
|
|
@@ -180,6 +229,49 @@ var init_sqlite = __esm({
|
|
|
180
229
|
}, (table) => ({
|
|
181
230
|
runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
|
|
182
231
|
}));
|
|
232
|
+
workplanSchedules2 = sqliteTable("workplan_schedules", {
|
|
233
|
+
id: text2("id").primaryKey(),
|
|
234
|
+
planId: text2("plan_id").notNull(),
|
|
235
|
+
name: text2("name").notNull(),
|
|
236
|
+
cronExpression: text2("cron_expression").notNull(),
|
|
237
|
+
timezone: text2("timezone").notNull(),
|
|
238
|
+
inputs: text2("inputs", { mode: "json" }).$type().notNull().default({}),
|
|
239
|
+
enabled: integer2("enabled", { mode: "boolean" }).notNull().default(true),
|
|
240
|
+
lastRunAt: text2("last_run_at"),
|
|
241
|
+
nextRunAt: text2("next_run_at"),
|
|
242
|
+
createdBy: text2("created_by"),
|
|
243
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
244
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
245
|
+
}, (table) => ({
|
|
246
|
+
dueIdx: index2("idx_workplan_schedules_due").on(table.nextRunAt)
|
|
247
|
+
}));
|
|
248
|
+
workplanRuns2 = sqliteTable("workplan_runs", {
|
|
249
|
+
id: text2("id").primaryKey(),
|
|
250
|
+
scheduleId: text2("schedule_id").references(() => workplanSchedules2.id),
|
|
251
|
+
planId: text2("plan_id").notNull(),
|
|
252
|
+
planName: text2("plan_name").notNull(),
|
|
253
|
+
status: text2("status").notNull().default("running"),
|
|
254
|
+
idempotencyKey: text2("idempotency_key"),
|
|
255
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
256
|
+
endedAt: text2("ended_at"),
|
|
257
|
+
error: text2("error")
|
|
258
|
+
}, (table) => ({
|
|
259
|
+
scheduleIdx: index2("idx_workplan_runs_schedule_id").on(table.scheduleId),
|
|
260
|
+
idempotencyUnique: uniqueIndex2("idx_workplan_runs_idempotency").on(table.idempotencyKey)
|
|
261
|
+
}));
|
|
262
|
+
workplanStepResults2 = sqliteTable("workplan_step_results", {
|
|
263
|
+
id: text2("id").primaryKey(),
|
|
264
|
+
workplanRunId: text2("workplan_run_id").notNull().references(() => workplanRuns2.id),
|
|
265
|
+
stepId: text2("step_id").notNull(),
|
|
266
|
+
stepName: text2("step_name").notNull(),
|
|
267
|
+
output: text2("output"),
|
|
268
|
+
exitCode: integer2("exit_code"),
|
|
269
|
+
durationMs: integer2("duration_ms"),
|
|
270
|
+
metadata: text2("metadata", { mode: "json" }).$type(),
|
|
271
|
+
createdAt: text2("created_at").notNull().default(nowDefault)
|
|
272
|
+
}, (table) => ({
|
|
273
|
+
runIdx: index2("idx_workplan_step_results_run_id").on(table.workplanRunId)
|
|
274
|
+
}));
|
|
183
275
|
runInputEvents2 = sqliteTable("run_input_events", {
|
|
184
276
|
id: integer2("id").primaryKey({ autoIncrement: true }),
|
|
185
277
|
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
@@ -196,7 +288,7 @@ var init_sqlite = __esm({
|
|
|
196
288
|
});
|
|
197
289
|
|
|
198
290
|
// ../db/src/sqlite-store.ts
|
|
199
|
-
import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
|
|
291
|
+
import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2, lte as lte2, sql as sql4 } from "drizzle-orm";
|
|
200
292
|
var init_sqlite_store = __esm({
|
|
201
293
|
"../db/src/sqlite-store.ts"() {
|
|
202
294
|
"use strict";
|
|
@@ -234,6 +326,149 @@ var init_client = __esm({
|
|
|
234
326
|
}
|
|
235
327
|
});
|
|
236
328
|
|
|
329
|
+
// ../db/src/baseline-migrations.ts
|
|
330
|
+
import { createHash } from "crypto";
|
|
331
|
+
import { readFileSync } from "fs";
|
|
332
|
+
import { resolve } from "path";
|
|
333
|
+
function readJournal(migrationsFolder) {
|
|
334
|
+
return JSON.parse(
|
|
335
|
+
readFileSync(resolve(migrationsFolder, "meta/_journal.json"), "utf8")
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function migrationHash(migrationsFolder, tag) {
|
|
339
|
+
const sql5 = readFileSync(resolve(migrationsFolder, `${tag}.sql`), "utf8");
|
|
340
|
+
return createHash("sha256").update(sql5).digest("hex");
|
|
341
|
+
}
|
|
342
|
+
function baselineEntries(migrationsFolder, upToIdx) {
|
|
343
|
+
const journal = readJournal(migrationsFolder);
|
|
344
|
+
return journal.entries.filter((entry) => entry.idx <= upToIdx).map((entry) => ({
|
|
345
|
+
hash: migrationHash(migrationsFolder, entry.tag),
|
|
346
|
+
createdAt: entry.when,
|
|
347
|
+
tag: entry.tag
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
async function tableExistsPg(pool, tableName) {
|
|
351
|
+
const result = await pool.query(
|
|
352
|
+
`SELECT EXISTS (
|
|
353
|
+
SELECT 1 FROM information_schema.tables
|
|
354
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
355
|
+
) AS exists`,
|
|
356
|
+
[tableName]
|
|
357
|
+
);
|
|
358
|
+
return Boolean(result.rows[0]?.exists);
|
|
359
|
+
}
|
|
360
|
+
async function getLastMigrationCreatedAtPg(pool) {
|
|
361
|
+
await pool.query("CREATE SCHEMA IF NOT EXISTS drizzle");
|
|
362
|
+
await pool.query(`
|
|
363
|
+
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
|
|
364
|
+
id SERIAL PRIMARY KEY,
|
|
365
|
+
hash text NOT NULL,
|
|
366
|
+
created_at bigint
|
|
367
|
+
)
|
|
368
|
+
`);
|
|
369
|
+
const result = await pool.query(
|
|
370
|
+
"SELECT created_at FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 1"
|
|
371
|
+
);
|
|
372
|
+
const value = result.rows[0]?.created_at;
|
|
373
|
+
return value === null || value === void 0 ? null : Number(value);
|
|
374
|
+
}
|
|
375
|
+
async function insertBaselinePg(pool, entries) {
|
|
376
|
+
const inserted = [];
|
|
377
|
+
const lastCreatedAt = await getLastMigrationCreatedAtPg(pool);
|
|
378
|
+
for (const entry of entries) {
|
|
379
|
+
if (lastCreatedAt !== null && lastCreatedAt >= entry.createdAt) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const existing = await pool.query(
|
|
383
|
+
"SELECT id FROM drizzle.__drizzle_migrations WHERE created_at = $1 LIMIT 1",
|
|
384
|
+
[entry.createdAt]
|
|
385
|
+
);
|
|
386
|
+
if (existing.rowCount) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
await pool.query(
|
|
390
|
+
'INSERT INTO drizzle.__drizzle_migrations ("hash", "created_at") VALUES ($1, $2)',
|
|
391
|
+
[entry.hash, entry.createdAt]
|
|
392
|
+
);
|
|
393
|
+
inserted.push(entry.tag);
|
|
394
|
+
}
|
|
395
|
+
return inserted;
|
|
396
|
+
}
|
|
397
|
+
async function baselineLegacyPostgresMigrations(pool, migrationsFolder) {
|
|
398
|
+
const hasTasks = await tableExistsPg(pool, "tasks");
|
|
399
|
+
if (!hasTasks) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const hasWorkplanSchedules = await tableExistsPg(pool, "workplan_schedules");
|
|
403
|
+
const upToIdx = hasWorkplanSchedules ? 1 : 0;
|
|
404
|
+
const inserted = await insertBaselinePg(pool, baselineEntries(migrationsFolder, upToIdx));
|
|
405
|
+
if (inserted.length > 0) {
|
|
406
|
+
process.stdout.write(
|
|
407
|
+
`Baselined existing Postgres schema for migration(s): ${inserted.join(", ")}
|
|
408
|
+
`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function tableExistsSqlite(db, tableName) {
|
|
413
|
+
const row = db.prepare(
|
|
414
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1"
|
|
415
|
+
).get(tableName);
|
|
416
|
+
return Boolean(row);
|
|
417
|
+
}
|
|
418
|
+
function getLastMigrationCreatedAtSqlite(db) {
|
|
419
|
+
db.exec(`
|
|
420
|
+
CREATE TABLE IF NOT EXISTS __drizzle_migrations (
|
|
421
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
422
|
+
hash text NOT NULL,
|
|
423
|
+
created_at numeric
|
|
424
|
+
)
|
|
425
|
+
`);
|
|
426
|
+
const row = db.prepare(
|
|
427
|
+
"SELECT created_at FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 1"
|
|
428
|
+
).get();
|
|
429
|
+
return row?.created_at ?? null;
|
|
430
|
+
}
|
|
431
|
+
function insertBaselineSqlite(db, entries) {
|
|
432
|
+
const inserted = [];
|
|
433
|
+
const lastCreatedAt = getLastMigrationCreatedAtSqlite(db);
|
|
434
|
+
const insert = db.prepare(
|
|
435
|
+
'INSERT INTO __drizzle_migrations ("hash", "created_at") VALUES (?, ?)'
|
|
436
|
+
);
|
|
437
|
+
const exists = db.prepare(
|
|
438
|
+
"SELECT 1 FROM __drizzle_migrations WHERE created_at = ? LIMIT 1"
|
|
439
|
+
);
|
|
440
|
+
for (const entry of entries) {
|
|
441
|
+
if (lastCreatedAt !== null && lastCreatedAt >= entry.createdAt) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (exists.get(entry.createdAt)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
insert.run(entry.hash, entry.createdAt);
|
|
448
|
+
inserted.push(entry.tag);
|
|
449
|
+
}
|
|
450
|
+
return inserted;
|
|
451
|
+
}
|
|
452
|
+
function baselineLegacySqliteMigrations(db, migrationsFolder) {
|
|
453
|
+
if (!tableExistsSqlite(db, "tasks")) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const hasWorkplanSchedules = tableExistsSqlite(db, "workplan_schedules");
|
|
457
|
+
const upToIdx = hasWorkplanSchedules ? 1 : 0;
|
|
458
|
+
const inserted = insertBaselineSqlite(db, baselineEntries(migrationsFolder, upToIdx));
|
|
459
|
+
if (inserted.length > 0) {
|
|
460
|
+
process.stdout.write(
|
|
461
|
+
`Baselined existing SQLite schema for migration(s): ${inserted.join(", ")}
|
|
462
|
+
`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
var init_baseline_migrations = __esm({
|
|
467
|
+
"../db/src/baseline-migrations.ts"() {
|
|
468
|
+
"use strict";
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
237
472
|
// ../db/src/migration.ts
|
|
238
473
|
var migration_exports = {};
|
|
239
474
|
__export(migration_exports, {
|
|
@@ -241,7 +476,7 @@ __export(migration_exports, {
|
|
|
241
476
|
});
|
|
242
477
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
243
478
|
import { homedir as homedir2 } from "os";
|
|
244
|
-
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
479
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
245
480
|
import { fileURLToPath } from "url";
|
|
246
481
|
async function migratePostgres(databaseUrl) {
|
|
247
482
|
const adminUrl = new URL(databaseUrl);
|
|
@@ -271,8 +506,10 @@ async function migratePostgres(databaseUrl) {
|
|
|
271
506
|
await adminPool.end();
|
|
272
507
|
}
|
|
273
508
|
const pool = getPool(databaseUrl);
|
|
509
|
+
const migrationsFolder = resolve2(distDir, "migrations/pg");
|
|
510
|
+
await baselineLegacyPostgresMigrations(pool, migrationsFolder);
|
|
274
511
|
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
275
|
-
await migrate(getDrizzle(pool), { migrationsFolder
|
|
512
|
+
await migrate(getDrizzle(pool), { migrationsFolder });
|
|
276
513
|
await pool.end();
|
|
277
514
|
process.stdout.write("Postgres migrations applied.\n");
|
|
278
515
|
}
|
|
@@ -287,8 +524,10 @@ async function migrateSqlite(databaseUrl) {
|
|
|
287
524
|
const sqlite = new Database2(filePath);
|
|
288
525
|
sqlite.pragma("journal_mode = WAL");
|
|
289
526
|
sqlite.pragma("synchronous = NORMAL");
|
|
527
|
+
const migrationsFolder = resolve2(distDir, "migrations/sqlite");
|
|
528
|
+
baselineLegacySqliteMigrations(sqlite, migrationsFolder);
|
|
290
529
|
const db = drizzle(sqlite, { schema: sqliteSchema });
|
|
291
|
-
migrate(db, { migrationsFolder
|
|
530
|
+
migrate(db, { migrationsFolder });
|
|
292
531
|
sqlite.close();
|
|
293
532
|
process.stdout.write(`SQLite migrations applied: ${filePath}
|
|
294
533
|
`);
|
|
@@ -305,13 +544,14 @@ var init_migration = __esm({
|
|
|
305
544
|
"../db/src/migration.ts"() {
|
|
306
545
|
"use strict";
|
|
307
546
|
init_client();
|
|
547
|
+
init_baseline_migrations();
|
|
308
548
|
distDir = dirname2(fileURLToPath(import.meta.url));
|
|
309
549
|
}
|
|
310
550
|
});
|
|
311
551
|
|
|
312
552
|
// ../cli/src/setup.ts
|
|
313
553
|
import { randomBytes } from "crypto";
|
|
314
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync, writeFileSync } from "fs";
|
|
554
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
315
555
|
import { homedir as homedir3 } from "os";
|
|
316
556
|
import { join as join3 } from "path";
|
|
317
557
|
import { createInterface } from "readline/promises";
|
|
@@ -323,7 +563,7 @@ function generateToken() {
|
|
|
323
563
|
function parseEnvFile(path) {
|
|
324
564
|
try {
|
|
325
565
|
const result = {};
|
|
326
|
-
for (const line of
|
|
566
|
+
for (const line of readFileSync2(path, "utf8").split("\n")) {
|
|
327
567
|
const t = line.trim();
|
|
328
568
|
if (!t || t.startsWith("#")) continue;
|
|
329
569
|
const eq3 = t.indexOf("=");
|