@malloy-publisher/create-malloy-package 0.0.1

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/index.js ADDED
@@ -0,0 +1,1869 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as crypto from "node:crypto";
5
+ import * as fs5 from "node:fs";
6
+ import * as path4 from "node:path";
7
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8
+ import { Command, Option } from "commander";
9
+
10
+ // src/errors.ts
11
+ class ScaffoldError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "ScaffoldError";
15
+ }
16
+ }
17
+
18
+ // src/log.ts
19
+ var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
20
+ function paint(code, text) {
21
+ return useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
22
+ }
23
+ var bold = (text) => paint("1", text);
24
+ var dim = (text) => paint("2", text);
25
+ var green = (text) => paint("32", text);
26
+ var cyan = (text) => paint("36", text);
27
+ var yellow = (text) => paint("33", text);
28
+ var red = (text) => paint("31", text);
29
+
30
+ // src/names.ts
31
+ var SAFE_NAME_RE = /^(?!\.\.?$)(?!\.)[A-Za-z0-9._-]+$/;
32
+ var MAX_FILENAME = 255;
33
+ var MAX_PACKAGE_NAME = MAX_FILENAME - ".malloy".length;
34
+ function validatePackageName(name) {
35
+ if (!SAFE_NAME_RE.test(name)) {
36
+ throw new ScaffoldError(`Invalid package name "${preview(name)}": use letters, digits, "-", ` + `"_", or "." and do not start with ".".`);
37
+ }
38
+ if (name.length > MAX_PACKAGE_NAME) {
39
+ throw new ScaffoldError(`Package name is ${name.length} characters; the limit is ` + `${MAX_PACKAGE_NAME}, because the package also holds a ` + `"<name>.malloy" model file and a filename cannot exceed ` + `${MAX_FILENAME} characters.`);
40
+ }
41
+ const modelFile = `${toMalloyIdentifier(name)}.malloy`;
42
+ if (modelFile.length > MAX_FILENAME) {
43
+ throw new ScaffoldError(`Package name "${preview(name)}" would need a model file named ` + `"${modelFile}", ` + `which is ${modelFile.length} characters and cannot be written ` + `(the limit is ${MAX_FILENAME}). Shorten the name by ` + `${modelFile.length - MAX_FILENAME}.`);
44
+ }
45
+ }
46
+ var MAX_ENVIRONMENT_NAME = 255;
47
+ function validateEnvironmentName(name) {
48
+ if (typeof name !== "string") {
49
+ throw new ScaffoldError(`publisher.config.json has an environment whose "name" is ` + `${name === null ? "null" : typeof name}, not a string. This tool ` + `writes that name into the start and reset commands, into ` + `AGENTS.md, and into the REST URLs it prints, where it would read ` + `as "${String(name)}". Give the environment a name, then run again.`);
50
+ }
51
+ if (!SAFE_NAME_RE.test(name)) {
52
+ throw new ScaffoldError(`Invalid environment name "${preview(name)}" in ` + `publisher.config.json${describeUnsafeCharacter(name)}: use ` + `letters, digits, "-", "_", or "." and do not start with ".". ` + `This tool writes the environment name into the ` + `--watch-env argument of the start and reset commands and into the ` + `shell blocks in AGENTS.md, so anything outside that set would be ` + `run as part of those commands. Publisher rejects the same names ` + `on its own URLs. Rename the environment, then run again.`);
53
+ }
54
+ if (name.length > MAX_ENVIRONMENT_NAME) {
55
+ throw new ScaffoldError(`Environment name "${preview(name)}" in publisher.config.json is ` + `${name.length} characters; the limit is ${MAX_ENVIRONMENT_NAME}, ` + `which is what Publisher accepts in a URL. Rename the environment, ` + `then run again.`);
56
+ }
57
+ }
58
+ function preview(value, max = 60) {
59
+ const shortened = value.length > max ? `${value.slice(0, max)}...` : value;
60
+ return printable(shortened);
61
+ }
62
+ function printable(value) {
63
+ let out = "";
64
+ for (const character of value) {
65
+ out += isControl(character) ? escapeCharacter(character) : character;
66
+ }
67
+ return out;
68
+ }
69
+ function isControl(character) {
70
+ const code = character.codePointAt(0);
71
+ return code < 32 || code === 127 || code >= 128 && code <= 159 || code === 8232 || code === 8233 || code === 8206 || code === 8207 || code === 1564 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297;
72
+ }
73
+ function escapeCharacter(character) {
74
+ switch (character) {
75
+ case `
76
+ `:
77
+ return "\\n";
78
+ case "\r":
79
+ return "\\r";
80
+ case "\t":
81
+ return "\\t";
82
+ default:
83
+ return `\\u${codePointHex(character)}`;
84
+ }
85
+ }
86
+ function codePointHex(character) {
87
+ const code = character.codePointAt(0);
88
+ return code.toString(16).toUpperCase().padStart(4, "0");
89
+ }
90
+ function describeUnsafeCharacter(name) {
91
+ for (const character of name) {
92
+ if (/[A-Za-z0-9._-]/.test(character)) {
93
+ continue;
94
+ }
95
+ const code = character.codePointAt(0);
96
+ const shown = character === " " ? "a space" : character.trim() === "" || code < 32 || code === 127 ? `U+${codePointHex(character)}` : `"${printable(character)}"`;
97
+ return ` (the first character it cannot hold is ${shown})`;
98
+ }
99
+ return "";
100
+ }
101
+ var MALLOY_RESERVED = new Set([
102
+ "all",
103
+ "and",
104
+ "as",
105
+ "asc",
106
+ "avg",
107
+ "boolean",
108
+ "by",
109
+ "case",
110
+ "cast",
111
+ "compose",
112
+ "count",
113
+ "date",
114
+ "day",
115
+ "desc",
116
+ "distinct",
117
+ "else",
118
+ "end",
119
+ "exclude",
120
+ "export",
121
+ "extend",
122
+ "false",
123
+ "filter",
124
+ "for",
125
+ "from",
126
+ "full",
127
+ "has",
128
+ "hour",
129
+ "import",
130
+ "in",
131
+ "include",
132
+ "inner",
133
+ "internal",
134
+ "is",
135
+ "json",
136
+ "left",
137
+ "like",
138
+ "max",
139
+ "min",
140
+ "minute",
141
+ "month",
142
+ "not",
143
+ "now",
144
+ "null",
145
+ "number",
146
+ "on",
147
+ "or",
148
+ "pick",
149
+ "private",
150
+ "public",
151
+ "quarter",
152
+ "right",
153
+ "second",
154
+ "source",
155
+ "sql",
156
+ "string",
157
+ "sum",
158
+ "table",
159
+ "then",
160
+ "this",
161
+ "timestamp",
162
+ "timestamptz",
163
+ "to",
164
+ "true",
165
+ "virtual",
166
+ "week",
167
+ "when",
168
+ "with",
169
+ "year",
170
+ "abs",
171
+ "acos",
172
+ "ascii",
173
+ "asin",
174
+ "atan",
175
+ "atan2",
176
+ "avg_moving",
177
+ "byte_length",
178
+ "ceil",
179
+ "chr",
180
+ "coalesce",
181
+ "concat",
182
+ "cos",
183
+ "dense_rank",
184
+ "div",
185
+ "ends_with",
186
+ "exp",
187
+ "first_value",
188
+ "floor",
189
+ "greatest",
190
+ "ifnull",
191
+ "is_inf",
192
+ "is_nan",
193
+ "lag",
194
+ "last_value",
195
+ "lead",
196
+ "least",
197
+ "length",
198
+ "ln",
199
+ "log",
200
+ "lower",
201
+ "ltrim",
202
+ "max_cumulative",
203
+ "max_window",
204
+ "min_cumulative",
205
+ "min_window",
206
+ "nullif",
207
+ "pi",
208
+ "pow",
209
+ "rand",
210
+ "rank",
211
+ "regexp_extract",
212
+ "replace",
213
+ "round",
214
+ "row_number",
215
+ "rtrim",
216
+ "sign",
217
+ "sin",
218
+ "sql_boolean",
219
+ "sql_date",
220
+ "sql_number",
221
+ "sql_string",
222
+ "sql_timestamp",
223
+ "sqrt",
224
+ "starts_with",
225
+ "stddev",
226
+ "string_repeat",
227
+ "strpos",
228
+ "substr",
229
+ "sum_cumulative",
230
+ "sum_moving",
231
+ "sum_window",
232
+ "tan",
233
+ "trim",
234
+ "trunc",
235
+ "unicode",
236
+ "upper"
237
+ ]);
238
+ function toMalloyIdentifier(name) {
239
+ let identifier = name.replace(/[^A-Za-z0-9_]/g, "_");
240
+ if (/^[0-9]/.test(identifier)) {
241
+ identifier = `_${identifier}`;
242
+ }
243
+ if (MALLOY_RESERVED.has(identifier.toLowerCase())) {
244
+ identifier = `${identifier}_source`;
245
+ }
246
+ return identifier;
247
+ }
248
+
249
+ // src/scaffold.ts
250
+ import * as fs4 from "node:fs";
251
+ import * as path3 from "node:path";
252
+
253
+ // src/config.ts
254
+ import * as fs from "node:fs";
255
+ function shown(value, max = 120) {
256
+ const text = typeof value === "string" ? value : String(value);
257
+ return printable(text.length > max ? `${text.slice(0, max)}...` : text);
258
+ }
259
+ function defaultConfig(envName, pkg) {
260
+ return {
261
+ frozenConfig: false,
262
+ environments: [
263
+ {
264
+ name: envName,
265
+ packages: pkg ? [pkg] : [],
266
+ connections: []
267
+ }
268
+ ]
269
+ };
270
+ }
271
+ function parseJson(text) {
272
+ return JSON.parse(text.replace(/^\uFEFF/, ""));
273
+ }
274
+ function asJsonObject(value) {
275
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
276
+ }
277
+ function describeJsonValue(value) {
278
+ if (value === null) {
279
+ return "null";
280
+ }
281
+ if (Array.isArray(value)) {
282
+ return "an array";
283
+ }
284
+ if (typeof value === "object") {
285
+ return "an object";
286
+ }
287
+ return `a ${typeof value}`;
288
+ }
289
+ function readConfig(configPath) {
290
+ let parsed;
291
+ try {
292
+ parsed = parseJson(fs.readFileSync(configPath, "utf8"));
293
+ } catch (err) {
294
+ throw new ScaffoldError(`Could not parse ${shown(configPath)}: ` + `${shown(err.message, 200)}. ` + `Fix or remove it, then run again.`);
295
+ }
296
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.environments)) {
297
+ throw new ScaffoldError(`${shown(configPath)} is not a Publisher config (no "environments" array). ` + `Fix or remove it, then run again.`);
298
+ }
299
+ const config = parsed;
300
+ assertConfigShape(configPath, config);
301
+ return config;
302
+ }
303
+ function assertConfigShape(configPath, config) {
304
+ const total = config.environments.length;
305
+ config.environments.forEach((env, index) => {
306
+ const where = `entry ${index + 1} of ${total} in "environments"`;
307
+ if (!asJsonObject(env)) {
308
+ throw new ScaffoldError(`${shown(configPath)} has an environment that is ` + `${describeJsonValue(env)} rather than a JSON object ` + `(${where}). Publisher cannot load that file, and this tool ` + `cannot register a package in it. Fix or remove it, then run ` + `again.`);
309
+ }
310
+ if (env.packages === undefined) {
311
+ return;
312
+ }
313
+ if (!Array.isArray(env.packages)) {
314
+ throw new ScaffoldError(`${shown(configPath)} has an environment whose "packages" is ` + `${describeJsonValue(env.packages)} rather than an array ` + `(${where}). Registering a package there would replace that ` + `value with a list holding only the new package, so nothing was ` + `written. Fix or remove it, then run again.`);
315
+ }
316
+ env.packages.forEach((pkg, pkgIndex) => {
317
+ if (!asJsonObject(pkg)) {
318
+ throw new ScaffoldError(`${shown(configPath)} has a package that is ${describeJsonValue(pkg)} ` + `rather than a JSON object (entry ${pkgIndex + 1} of ` + `${env.packages.length} in the "packages" of ` + `${where}). Publisher cannot load that file, and this tool ` + `cannot tell whether the name you asked for is already taken ` + `by it. Fix or remove it, then run again.`);
319
+ }
320
+ });
321
+ });
322
+ }
323
+ function writeConfig(configPath, config) {
324
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + `
325
+ `);
326
+ }
327
+ function targetEnvironment(config, envName) {
328
+ const environments = config.environments.filter((candidate) => asJsonObject(candidate) !== undefined);
329
+ return environments.find((candidate) => candidate.name === envName) ?? environments[0];
330
+ }
331
+ function resolveEnvironmentName(config, envName) {
332
+ return targetEnvironment(config, envName)?.name ?? envName;
333
+ }
334
+ function environmentPackageCount(config, envName) {
335
+ const env = targetEnvironment(config, envName);
336
+ return Array.isArray(env?.packages) ? env.packages.length : 0;
337
+ }
338
+ function environmentPackageNames(config, envName) {
339
+ const env = targetEnvironment(config, envName);
340
+ if (!Array.isArray(env?.packages)) {
341
+ return [];
342
+ }
343
+ return env.packages.map((entry) => asJsonObject(entry)?.name).filter((name) => typeof name === "string");
344
+ }
345
+ function assertCanAddPackage(config, envName, pkg) {
346
+ if (config.frozenConfig === true) {
347
+ throw new ScaffoldError(`publisher.config.json has "frozenConfig": true, which forbids adding ` + `packages. Set it to false, or register "${shown(pkg.name)}" by hand.`);
348
+ }
349
+ const env = targetEnvironment(config, envName);
350
+ const existing = Array.isArray(env?.packages) ? env.packages.find((entry) => asJsonObject(entry)?.name === pkg.name) : undefined;
351
+ if (existing && existing.location !== pkg.location) {
352
+ throw new ScaffoldError(`Package "${shown(pkg.name)}" is already registered in environment ` + `"${shown(env?.name)}" at a different location ` + `("${shown(existing.location)}"). ` + `Choose another name.`);
353
+ }
354
+ }
355
+ function addPackage(config, envName, pkg) {
356
+ assertCanAddPackage(config, envName, pkg);
357
+ let env = targetEnvironment(config, envName);
358
+ if (!env) {
359
+ env = { name: envName, packages: [], connections: [] };
360
+ config.environments.push(env);
361
+ }
362
+ if (!Array.isArray(env.packages)) {
363
+ env.packages = [];
364
+ }
365
+ if (env.packages.some((entry) => asJsonObject(entry)?.name === pkg.name)) {
366
+ return { added: false, envName: env.name };
367
+ }
368
+ env.packages.push(pkg);
369
+ return { added: true, envName: env.name };
370
+ }
371
+
372
+ // src/skills.ts
373
+ import * as fs2 from "node:fs";
374
+ import * as path from "node:path";
375
+ import { skillsDir } from "@malloy-publisher/skills";
376
+ function assertSkillsAvailable() {
377
+ if (countSkills(skillsDir) === 0) {
378
+ throw new ScaffoldError(`No Malloy skills found in ${skillsDir}. The @malloy-publisher/skills ` + `install looks incomplete; reinstall create-malloy-package and run again.`);
379
+ }
380
+ }
381
+ function installSkills(targetDir, workspaceRoot) {
382
+ const empty = { installed: 0, skipped: [], refreshed: [], removed: [] };
383
+ if (isSymlink(targetDir)) {
384
+ return {
385
+ ...empty,
386
+ refused: "it is a symlink, so writing into it would edit whatever it points at"
387
+ };
388
+ }
389
+ if (!isWithinDirectory(targetDir, workspaceRoot)) {
390
+ return {
391
+ ...empty,
392
+ refused: "it resolves outside the workspace, so writing into it would put " + "files somewhere else on this machine"
393
+ };
394
+ }
395
+ fs2.mkdirSync(targetDir, { recursive: true });
396
+ const skipped = [];
397
+ const refreshed = [];
398
+ const removed = [];
399
+ let installed = 0;
400
+ for (const entry of fs2.readdirSync(skillsDir, { withFileTypes: true })) {
401
+ if (!entry.isDirectory()) {
402
+ continue;
403
+ }
404
+ const source = path.join(skillsDir, entry.name);
405
+ if (!fs2.existsSync(path.join(source, "SKILL.md"))) {
406
+ continue;
407
+ }
408
+ const target = path.join(targetDir, entry.name);
409
+ if (isSymlink(target)) {
410
+ skipped.push(entry.name);
411
+ continue;
412
+ }
413
+ if (fs2.existsSync(target)) {
414
+ refreshed.push(entry.name);
415
+ for (const relative of pathsNotIn(target, source)) {
416
+ removed.push(path.join(entry.name, relative));
417
+ }
418
+ }
419
+ fs2.rmSync(target, { recursive: true, force: true });
420
+ fs2.cpSync(source, target, { recursive: true });
421
+ installed += 1;
422
+ }
423
+ return { installed, skipped, refreshed, removed };
424
+ }
425
+ function pathsNotIn(target, source, prefix = "") {
426
+ let entries;
427
+ try {
428
+ entries = fs2.readdirSync(path.join(target, prefix), {
429
+ withFileTypes: true
430
+ });
431
+ } catch {
432
+ return [];
433
+ }
434
+ const missing = [];
435
+ for (const entry of entries) {
436
+ const relative = path.join(prefix, entry.name);
437
+ if (!fs2.existsSync(path.join(source, relative))) {
438
+ missing.push(entry.isDirectory() ? relative + path.sep : relative);
439
+ continue;
440
+ }
441
+ if (entry.isDirectory()) {
442
+ missing.push(...pathsNotIn(target, source, relative));
443
+ }
444
+ }
445
+ return missing;
446
+ }
447
+ function countSkills(dir) {
448
+ if (!fs2.existsSync(dir)) {
449
+ return 0;
450
+ }
451
+ return fs2.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs2.existsSync(path.join(dir, entry.name, "SKILL.md"))).length;
452
+ }
453
+ function isWithinDirectory(target, root) {
454
+ const realRoot = realPathOf(root);
455
+ const realTarget = realPathOf(target);
456
+ return realTarget === realRoot || realTarget.startsWith(realRoot + path.sep);
457
+ }
458
+ function realPathOf(target, hops = 0) {
459
+ const resolved = path.resolve(target);
460
+ const parent = path.dirname(resolved);
461
+ if (parent === resolved) {
462
+ return resolved;
463
+ }
464
+ const here = path.join(realPathOf(parent, hops), path.basename(resolved));
465
+ if (hops >= 40) {
466
+ return here;
467
+ }
468
+ try {
469
+ if (!fs2.lstatSync(here).isSymbolicLink()) {
470
+ return here;
471
+ }
472
+ return realPathOf(path.resolve(path.dirname(here), fs2.readlinkSync(here)), hops + 1);
473
+ } catch {
474
+ return here;
475
+ }
476
+ }
477
+ function isSymlink(target) {
478
+ try {
479
+ return fs2.lstatSync(target).isSymbolicLink();
480
+ } catch {
481
+ return false;
482
+ }
483
+ }
484
+
485
+ // src/templates.ts
486
+ import * as fs3 from "node:fs";
487
+ import * as path2 from "node:path";
488
+ import { fileURLToPath } from "node:url";
489
+ var templatesDir = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..", "templates");
490
+ function renderTemplate(name, vars) {
491
+ const raw = fs3.readFileSync(path2.join(templatesDir, name), "utf8");
492
+ const referenced = new Set;
493
+ const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
494
+ if (!(key in vars)) {
495
+ throw new Error(`Template ${name} references unknown variable {{${key}}}`);
496
+ }
497
+ referenced.add(key);
498
+ return vars[key];
499
+ });
500
+ const unused = Object.keys(vars).filter((key) => !referenced.has(key));
501
+ if (unused.length > 0) {
502
+ const names = unused.map((key) => `{{${key}}}`).join(", ");
503
+ const one = unused.length === 1;
504
+ throw new Error(`Template ${name} never references ${names}, so ` + `${one ? "that value is" : "those values are"} computed and then ` + `dropped. Either use ${one ? "it" : "them"} in the template or ` + `stop passing ${one ? "it" : "them"}.`);
505
+ }
506
+ return rendered;
507
+ }
508
+
509
+ // src/scaffold.ts
510
+ var MALLOY_AGENTS_FILE = "AGENTS.malloy.md";
511
+ var BRIEFING_MARKER = "This directory is a [Malloy Publisher](https://github.com/malloydata/publisher)";
512
+ var ENV_NAME = "default";
513
+ var PUBLISHER_PORT = 4000;
514
+ var MCP_PORT = 4040;
515
+ var ALT_PUBLISHER_PORT = PUBLISHER_PORT + 100;
516
+ var ALT_MCP_PORT = MCP_PORT + 100;
517
+ var BIND_HOST = "127.0.0.1";
518
+ var SERVER_VERSION = "0.0.231";
519
+ function startCommandFor(envName) {
520
+ return `npx -y @malloy-publisher/server@${SERVER_VERSION} --server_root . ` + `--config ./publisher.config.json --host ${BIND_HOST} ` + `--watch-env ${envName}`;
521
+ }
522
+ function resetCommandFor(envName) {
523
+ return `${startCommandFor(envName)} --init`;
524
+ }
525
+ var DUCKDB_SAFE_PATH_CHAR = /[A-Za-z0-9._~:/?#@!$&*+,=%-]/;
526
+ var RESERVED_PACKAGE_NAMES = new Set([
527
+ "AGENTS.md",
528
+ MALLOY_AGENTS_FILE,
529
+ "CLAUDE.md",
530
+ "node_modules",
531
+ "package.json",
532
+ "package-lock.json",
533
+ "publisher.config.json",
534
+ "publisher.db",
535
+ "publisher_data"
536
+ ].map((name) => name.toLowerCase()));
537
+ function scaffold(options) {
538
+ assertServablePath(options.cwd);
539
+ assertSkillsAvailable();
540
+ if (options.name === undefined && options.dataFile !== undefined) {
541
+ throw new ScaffoldError(`--data seeds a new package, so it needs a package name: ` + `create-malloy-package <name> --data ${options.dataFile}`);
542
+ }
543
+ if (options.name !== undefined) {
544
+ validatePackageName(options.name);
545
+ assertNotReservedName(options.name);
546
+ }
547
+ const mcpConfigPath = mcpConfigPathFor(options.host);
548
+ assertWorkspacePathsContained(options, mcpConfigPath);
549
+ if (options.force) {
550
+ readMergeablePackageJson(options.cwd);
551
+ }
552
+ const result = {
553
+ cwd: options.cwd,
554
+ packageCreated: false,
555
+ envName: ENV_NAME,
556
+ envPackageCount: 0,
557
+ publisherPort: PUBLISHER_PORT,
558
+ mcpPort: MCP_PORT,
559
+ host: options.host,
560
+ written: [],
561
+ skipped: [],
562
+ replaced: [],
563
+ agentsFile: "AGENTS.md",
564
+ configExtended: false,
565
+ needsReset: false,
566
+ hasStartScript: false,
567
+ startCommand: startCommandFor(ENV_NAME),
568
+ hasResetScript: false,
569
+ resetCommand: resetCommandFor(ENV_NAME),
570
+ mcpConfigPath,
571
+ mcpWired: false,
572
+ mcpMerged: false,
573
+ mcpOtherServers: 0,
574
+ mcpEntryReplaced: false,
575
+ skillsInstalled: 0
576
+ };
577
+ const configPath = path3.join(options.cwd, "publisher.config.json");
578
+ const configExists = fs4.existsSync(configPath);
579
+ const config = configExists ? readConfig(configPath) : defaultConfig(ENV_NAME);
580
+ const targetEnv = targetEnvironment(config, ENV_NAME);
581
+ if (targetEnv !== undefined) {
582
+ validateEnvironmentName(targetEnv.name);
583
+ }
584
+ result.envName = resolveEnvironmentName(config, ENV_NAME);
585
+ let packageEntry;
586
+ if (options.name !== undefined) {
587
+ packageEntry = { name: options.name, location: `./${options.name}` };
588
+ if (configExists) {
589
+ assertCanAddPackage(config, ENV_NAME, packageEntry);
590
+ }
591
+ createPackage(options, result);
592
+ }
593
+ wireWorkspace(options, config, configExists, configPath, packageEntry, result);
594
+ result.envPackageCount = environmentPackageCount(config, ENV_NAME);
595
+ return result;
596
+ }
597
+ function createPackage(options, result) {
598
+ const name = options.name;
599
+ if (options.dataFile !== undefined) {
600
+ validateDataFile(options.dataFile);
601
+ }
602
+ const sourceName = toMalloyIdentifier(name);
603
+ const modelFile = `${sourceName}.malloy`;
604
+ const packageDir = path3.join(options.cwd, name);
605
+ const packageDirExists = fs4.existsSync(packageDir);
606
+ if (packageDirExists && !fs4.lstatSync(packageDir).isDirectory()) {
607
+ throw new ScaffoldError(`"${name}" already exists here and is not a directory, so the package ` + `cannot be created there. --force does not help: it overwrites files ` + `inside a package directory, it does not replace something that is ` + `in the way.
608
+
609
+ ` + `Choose another name, or move or remove "${name}" first.`);
610
+ }
611
+ if (packageDirExists && !options.force) {
612
+ throw new ScaffoldError(`Directory "${name}" already exists. Choose another name, or re-run ` + `with --force.
613
+
614
+ ` + forceDescription(name, modelFile, options.host) + `
615
+
616
+ ` + `If you passed --force to \`npm create\`, npm read it as one of its ` + `own settings and it never reached this tool. Options need a \`--\` ` + `in front of them there:
617
+ ` + ` npm create @malloy-publisher/malloy-package ${name} -- --force`);
618
+ }
619
+ const dataDir = path3.join(packageDir, "data");
620
+ assertWithinWorkspace(dataDir, options.cwd, `${name}/data`);
621
+ fs4.mkdirSync(dataDir, { recursive: true });
622
+ writeFile(path3.join(packageDir, "publisher.json"), JSON.stringify({ name }, null, 2) + `
623
+ `, options.cwd);
624
+ let dataPath;
625
+ if (options.dataFile !== undefined) {
626
+ const sourceBase = path3.basename(options.dataFile);
627
+ const destBase = sanitizeFileName(sourceBase);
628
+ copyFile(options.dataFile, path3.join(dataDir, destBase), options.cwd, `${name}/data/${destBase}`);
629
+ dataPath = `data/${destBase}`;
630
+ if (destBase !== sourceBase) {
631
+ result.dataFileRenamedFrom = sourceBase;
632
+ }
633
+ writeFile(path3.join(packageDir, modelFile), renderTemplate("model.custom.malloy", { sourceName, dataPath }), options.cwd);
634
+ } else {
635
+ copyFile(path3.join(templatesDir, "sales.csv"), path3.join(dataDir, "sales.csv"), options.cwd, `${name}/data/sales.csv`);
636
+ dataPath = "data/sales.csv";
637
+ writeFile(path3.join(packageDir, modelFile), renderTemplate("model.default.malloy", { sourceName }), options.cwd);
638
+ }
639
+ result.packageCreated = true;
640
+ result.packageName = name;
641
+ result.sourceName = sourceName;
642
+ result.modelFile = modelFile;
643
+ result.dataPath = dataPath;
644
+ result.written.push(`${name}/`);
645
+ if (packageDirExists) {
646
+ result.replaced.push(`${name}/`);
647
+ }
648
+ }
649
+ function forceDescription(name, modelFile, host) {
650
+ const agentFiles = host === "cursor" ? "AGENTS.md" : "AGENTS.md and CLAUDE.md";
651
+ const mcpPath = mcpConfigPathFor(host);
652
+ return `--force does not empty the directory. It rewrites ${name}/publisher.json, ` + `${name}/${modelFile} and the data file it copies into ${name}/data/, and ` + `leaves anything else in there alone. It also refreshes .claude/skills/ ` + `from the bundled copies, as every run does. Outside the package it ` + `replaces ${agentFiles}; in package.json it sets only the "start" and ` + `"reset" scripts and keeps the rest of the file as it is; in ${mcpPath} it ` + `sets only the "malloy" server and keeps the others. publisher.config.json ` + `is extended, never rewritten, and .gitignore only gains the lines it is ` + `missing, with or without --force. None of those merges is a rewrite even ` + `when the file cannot be read: a package.json that is not valid JSON aborts ` + `the run before anything is written, and an unreadable ${mcpPath} or ` + `.gitignore is left alone and reported. If this directory has an AGENTS.md ` + `of its own, --force is not what you want for it: a run without --force ` + `keeps that file and writes the Publisher briefing to ${MALLOY_AGENTS_FILE} ` + `beside it instead, regenerating that one on every run.`;
653
+ }
654
+ function mcpConfigPathFor(host) {
655
+ return host === "cursor" ? ".cursor/mcp.json" : ".mcp.json";
656
+ }
657
+ function wireWorkspace(options, config, configExists, configPath, packageEntry, result) {
658
+ const { cwd } = options;
659
+ if (packageEntry) {
660
+ const registration = addPackage(config, ENV_NAME, packageEntry);
661
+ result.envName = registration.envName;
662
+ if (!configExists || registration.added) {
663
+ writeConfig(configPath, config);
664
+ result.configExtended = configExists;
665
+ result.written.push(configExists ? "publisher.config.json (extended)" : "publisher.config.json");
666
+ } else {
667
+ result.skipped.push("publisher.config.json (already registered)");
668
+ }
669
+ } else if (!configExists) {
670
+ writeConfig(configPath, config);
671
+ result.written.push("publisher.config.json");
672
+ } else {
673
+ result.skipped.push("publisher.config.json");
674
+ }
675
+ result.needsReset = result.packageCreated && (fs4.existsSync(path3.join(cwd, "publisher_data")) || fs4.existsSync(path3.join(cwd, "publisher.db")));
676
+ result.startCommand = startCommandFor(result.envName);
677
+ result.resetCommand = resetCommandFor(result.envName);
678
+ writeWorkspacePackageJson(cwd, options, result);
679
+ const scripts = workspaceScripts(cwd);
680
+ const declinedStart = declineReasonFor(scripts?.start, result.envName, false);
681
+ const declinedReset = declineReasonFor(scripts?.reset, result.envName, true);
682
+ result.hasStartScript = typeof scripts?.start === "string" && declinedStart === undefined;
683
+ result.declinedStartScript = declinedStart;
684
+ result.hasResetScript = typeof scripts?.reset === "string" && declinedReset === undefined;
685
+ result.declinedResetScript = declinedReset;
686
+ writeMcpConfig(cwd, result.mcpConfigPath, result);
687
+ installWorkspaceSkills(cwd, result);
688
+ mergeGitignore(cwd, result);
689
+ writeAgentsFile(cwd, renderAgentsFile(result, options.host, environmentPackageNames(config, ENV_NAME)), options, result);
690
+ if (options.host !== "cursor") {
691
+ writeIfAbsent(cwd, "CLAUDE.md", claudeFile(result.agentsFile), options, result);
692
+ }
693
+ }
694
+ function writeAgentsFile(cwd, content, options, result) {
695
+ assertBriefingIsRecognisable(content);
696
+ const relPath = "AGENTS.md";
697
+ const target = path3.join(cwd, relPath);
698
+ const exists = fs4.existsSync(target);
699
+ const ours = exists && isGeneratedBriefing(target);
700
+ if (!exists || ours || options.force) {
701
+ writeFile(target, content, cwd);
702
+ result.written.push(relPath);
703
+ if (ours) {
704
+ result.replaced.push(`${relPath} (regenerated, as every run does)`);
705
+ } else if (exists) {
706
+ result.replaced.push(relPath);
707
+ }
708
+ if (fs4.existsSync(path3.join(cwd, MALLOY_AGENTS_FILE))) {
709
+ result.skipped.push(`${MALLOY_AGENTS_FILE} (left from an earlier run, when AGENTS.md ` + `was not ours to write; AGENTS.md now holds the briefing, so ` + `this copy is stale and can be deleted)`);
710
+ }
711
+ return;
712
+ }
713
+ result.skipped.push(relPath);
714
+ const existed = fs4.existsSync(path3.join(cwd, MALLOY_AGENTS_FILE));
715
+ writeFile(path3.join(cwd, MALLOY_AGENTS_FILE), content, cwd);
716
+ result.agentsFile = MALLOY_AGENTS_FILE;
717
+ result.written.push(`${MALLOY_AGENTS_FILE} (the Publisher briefing, because this ` + `directory's own AGENTS.md was left alone)`);
718
+ if (existed) {
719
+ result.replaced.push(`${MALLOY_AGENTS_FILE} (regenerated, as every run does)`);
720
+ }
721
+ }
722
+ function isGeneratedBriefing(target) {
723
+ try {
724
+ return fs4.readFileSync(target, "utf8").includes(BRIEFING_MARKER);
725
+ } catch {
726
+ return false;
727
+ }
728
+ }
729
+ function assertBriefingIsRecognisable(content) {
730
+ if (!content.includes(BRIEFING_MARKER)) {
731
+ throw new Error(`The rendered agent briefing no longer holds the line "` + `${BRIEFING_MARKER}". That line is how a later run recognises a ` + `briefing this tool wrote and regenerates it in place, so without ` + `it every AGENTS.md written so far would be read as the user's own ` + `and the briefing diverted to ${MALLOY_AGENTS_FILE}. Restore the ` + `line in templates/AGENTS.md, or update BRIEFING_MARKER to a line ` + `the template still has.`);
732
+ }
733
+ }
734
+ function installWorkspaceSkills(cwd, result) {
735
+ const relDir = path3.join(".claude", "skills");
736
+ const skills = installSkills(path3.join(cwd, relDir), cwd);
737
+ result.skillsInstalled = skills.installed;
738
+ if (skills.refused) {
739
+ result.skipped.push(`${relDir}${path3.sep} (${skills.refused}; no skills were installed)`);
740
+ }
741
+ for (const name of skills.skipped) {
742
+ result.skipped.push(`${path3.join(relDir, name)} (symlink, not written through)`);
743
+ }
744
+ if (skills.refreshed.length > 0) {
745
+ const count = skills.refreshed.length;
746
+ result.replaced.push(`${relDir}${path3.sep} (${count} skill${count === 1 ? "" : "s"} ` + `refreshed: every run deletes each skill directory and writes the ` + `bundled copy back, so edits to a skill are reverted and anything ` + `else you put inside one is removed, not just changed)`);
747
+ }
748
+ if (skills.removed.length > 0) {
749
+ const shown2 = skills.removed.slice(0, 5).map((entry) => printable(path3.join(relDir, entry)));
750
+ const rest = skills.removed.length - shown2.length;
751
+ result.replaced.push(`${shown2.join(", ")}${rest > 0 ? `, and ${rest} more` : ""} (not ` + `part of any bundled skill, so nothing wrote ` + `${shown2.length === 1 && rest === 0 ? "it" : "them"} back)`);
752
+ }
753
+ }
754
+ function assertServablePath(cwd) {
755
+ const absolute = path3.resolve(cwd);
756
+ const candidate = path3.sep === "\\" ? absolute.replace(/\\/g, "/") : absolute;
757
+ for (const character of candidate) {
758
+ if (!DUCKDB_SAFE_PATH_CHAR.test(character)) {
759
+ throw new ScaffoldError(`This workspace cannot live in ${printable(absolute)}: DuckDB ` + `cannot read a ` + `data file under a path containing ${describeCharacter(character)}, so every model here would fail to load with the server ` + `still reporting healthy. Move to a path made of letters, ` + `digits, "-", "_", "." and "/" (for example ` + `~/malloy-workspace) and run again.`);
760
+ }
761
+ }
762
+ }
763
+ function describeCharacter(character) {
764
+ const named = {
765
+ " ": "a space",
766
+ "'": "an apostrophe",
767
+ '"': "a double quote",
768
+ "\\": "a backslash",
769
+ "\t": "a tab"
770
+ };
771
+ const label = named[character] ?? `"${character}"`;
772
+ const codePoint = character.codePointAt(0);
773
+ return codePoint < 32 || codePoint > 126 ? `${label} (U+${codePoint.toString(16).toUpperCase().padStart(4, "0")})` : label;
774
+ }
775
+ function assertWorkspacePathsContained(options, mcpConfigPath) {
776
+ const relatives = [
777
+ "publisher.config.json",
778
+ "package.json",
779
+ ".gitignore",
780
+ "AGENTS.md",
781
+ MALLOY_AGENTS_FILE,
782
+ mcpConfigPath
783
+ ];
784
+ if (options.host !== "cursor") {
785
+ relatives.push("CLAUDE.md");
786
+ }
787
+ if (options.name !== undefined) {
788
+ relatives.push(options.name);
789
+ }
790
+ for (const relative2 of relatives) {
791
+ assertWithinWorkspace(path3.join(options.cwd, relative2), options.cwd, relative2);
792
+ }
793
+ }
794
+ function assertWithinWorkspace(target, root, label) {
795
+ if (!isWithinDirectory(target, root)) {
796
+ throw new ScaffoldError(`"${label}" in ${root} is a symlink (or sits under one) pointing ` + `outside the workspace, so writing it would change a file somewhere ` + `else on this machine. Nothing was written. Remove or repoint it, ` + `then run again.`);
797
+ }
798
+ }
799
+ function assertNotReservedName(name) {
800
+ if (RESERVED_PACKAGE_NAMES.has(name.toLowerCase())) {
801
+ throw new ScaffoldError(`Package name "${name}" is reserved: the workspace itself uses that ` + `path. Choose another name.`);
802
+ }
803
+ }
804
+ function readWorkspacePackageJson(cwd) {
805
+ const target = path3.join(cwd, "package.json");
806
+ if (!fs4.existsSync(target)) {
807
+ return { kind: "absent" };
808
+ }
809
+ let text;
810
+ try {
811
+ text = fs4.readFileSync(target, "utf8");
812
+ } catch (err) {
813
+ return {
814
+ kind: "unusable",
815
+ problem: `unreadable: ${printable(err.message)}`
816
+ };
817
+ }
818
+ let parsed;
819
+ try {
820
+ parsed = parseJson(text);
821
+ } catch (err) {
822
+ return {
823
+ kind: "unusable",
824
+ problem: `not valid JSON: ${printable(err.message)}`
825
+ };
826
+ }
827
+ const pkg = asJsonObject(parsed);
828
+ return pkg ? { kind: "object", pkg } : {
829
+ kind: "unusable",
830
+ problem: `${describeJsonValue(parsed)}, not a JSON object`
831
+ };
832
+ }
833
+ function workspaceScripts(cwd) {
834
+ const manifest = readWorkspacePackageJson(cwd);
835
+ return manifest.kind === "object" ? asJsonObject(manifest.pkg.scripts) : undefined;
836
+ }
837
+ function isPublisherScript(script, envName, requireInit = false) {
838
+ return typeof script === "string" && declineReasonFor(script, envName, requireInit) === undefined;
839
+ }
840
+ function declineReasonFor(script, envName, requireInit = false) {
841
+ if (typeof script !== "string") {
842
+ return;
843
+ }
844
+ if (!script.includes("@malloy-publisher/server")) {
845
+ return { script, reason: "not-publisher" };
846
+ }
847
+ if (UNMODELLED_SHELL_SYNTAX.test(script)) {
848
+ return { script, reason: "unmodelled-shell-syntax" };
849
+ }
850
+ if (hasUnterminatedQuote(script)) {
851
+ return { script, reason: "unparseable" };
852
+ }
853
+ if (!isSingleServerInvocation(script)) {
854
+ return { script, reason: "unrecognised-shape" };
855
+ }
856
+ const host = hostFlagOf(script);
857
+ if (host === undefined) {
858
+ const joined = joinedHostFlagOf(script);
859
+ return joined === undefined ? { script, reason: "no-host-flag" } : { script, reason: "no-host-flag", joinedHostFlag: joined };
860
+ }
861
+ if (!isLoopbackHost(host)) {
862
+ return { script, reason: "non-loopback-host", host };
863
+ }
864
+ if (requireInit && !script.includes("--init")) {
865
+ return { script, reason: "no-init-flag", host };
866
+ }
867
+ if (!matchesGeneratedInvocation(script, envName, requireInit)) {
868
+ return { script, reason: "different-invocation" };
869
+ }
870
+ return;
871
+ }
872
+ var GENERATED_FLAGS = {
873
+ "--server_root": (value) => value === "." || value === "./",
874
+ "--config": (value) => value === "./publisher.config.json" || value === "publisher.config.json",
875
+ "--host": (value) => isLoopbackHost(value),
876
+ "--watch-env": (value, envName) => value === envName
877
+ };
878
+ var GENERATED_BOOLEAN_FLAGS = new Set(["--init"]);
879
+ function matchesGeneratedInvocation(script, envName, requireInit) {
880
+ const tokens = splitOnShellWhitespace(script);
881
+ const index = tokens.findIndex((token) => unquote(token).startsWith("@malloy-publisher/server"));
882
+ if (index === -1) {
883
+ return false;
884
+ }
885
+ const values = new Map;
886
+ let init = false;
887
+ const rest = tokens.slice(index + 1).map(unquote);
888
+ for (let i = 0;i < rest.length; i++) {
889
+ const flag = rest[i];
890
+ if (GENERATED_BOOLEAN_FLAGS.has(flag)) {
891
+ init = init || flag === "--init";
892
+ continue;
893
+ }
894
+ const accepts = GENERATED_FLAGS[flag];
895
+ if (accepts === undefined) {
896
+ return false;
897
+ }
898
+ const value = rest[i + 1];
899
+ if (value === undefined) {
900
+ return false;
901
+ }
902
+ values.set(flag, value);
903
+ i++;
904
+ }
905
+ for (const [flag, accepts] of Object.entries(GENERATED_FLAGS)) {
906
+ const value = values.get(flag);
907
+ if (value === undefined || !accepts(value, envName)) {
908
+ return false;
909
+ }
910
+ }
911
+ return init === requireInit;
912
+ }
913
+ function unquote(token) {
914
+ return token.replace(/^["']|["']$/g, "");
915
+ }
916
+ var SHELL_CHAINING_CHARACTERS = /[&|;<>\n\r]/;
917
+ var UNMODELLED_SHELL_SYNTAX = /[`$()#\\]/;
918
+ var SH_WHITESPACE = /[ \t\n]/;
919
+ function splitOnShellWhitespace(script) {
920
+ return script.split(/[ \t\n]+/).filter(Boolean);
921
+ }
922
+ var SCRIPT_RUNNER_PREFIXES = [
923
+ ["npx"],
924
+ ["bunx"],
925
+ ["pnpx"],
926
+ ["pnpm", "dlx"],
927
+ ["yarn", "dlx"],
928
+ ["bun", "x"]
929
+ ];
930
+ function runnerPrefixLength(before) {
931
+ for (const prefix of SCRIPT_RUNNER_PREFIXES) {
932
+ if (prefix.every((word, i) => before[i] === word)) {
933
+ return prefix.length;
934
+ }
935
+ }
936
+ return 0;
937
+ }
938
+ var RUNNER_BOOLEAN_FLAGS = new Set([
939
+ "-y",
940
+ "--yes",
941
+ "-q",
942
+ "--quiet",
943
+ "--silent"
944
+ ]);
945
+ function isSingleServerInvocation(script) {
946
+ if (SHELL_CHAINING_CHARACTERS.test(script)) {
947
+ return false;
948
+ }
949
+ const tokens = splitOnShellWhitespace(script);
950
+ const index = tokens.findIndex((token) => token.includes("@malloy-publisher/server"));
951
+ if (index === -1) {
952
+ return false;
953
+ }
954
+ const command = tokens[index].replace(/^["']|["']$/g, "");
955
+ if (command !== "@malloy-publisher/server") {
956
+ if (!command.startsWith("@malloy-publisher/server@")) {
957
+ return false;
958
+ }
959
+ const spec = command.slice("@malloy-publisher/server@".length);
960
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(spec)) {
961
+ return false;
962
+ }
963
+ if (/\.(?:tgz|tar\.gz|tar)$/i.test(spec)) {
964
+ return false;
965
+ }
966
+ }
967
+ const before = tokens.slice(0, index);
968
+ const runnerLength = runnerPrefixLength(before);
969
+ if (runnerLength === 0) {
970
+ return false;
971
+ }
972
+ return before.slice(runnerLength).every((token) => RUNNER_BOOLEAN_FLAGS.has(token));
973
+ }
974
+ var SERVER_VALUE_FLAGS = new Set([
975
+ "--port",
976
+ "--host",
977
+ "--server_root",
978
+ "--config",
979
+ "--mcp_port",
980
+ "--shutdown_drain_duration_seconds",
981
+ "--shutdown_graceful_close_timeout_seconds",
982
+ "--watch-env"
983
+ ]);
984
+ function hasUnterminatedQuote(script) {
985
+ let quote;
986
+ for (const char of script) {
987
+ if (quote !== undefined) {
988
+ if (char === quote)
989
+ quote = undefined;
990
+ continue;
991
+ }
992
+ if (char === '"' || char === "'")
993
+ quote = char;
994
+ }
995
+ return quote !== undefined;
996
+ }
997
+ function shellTokens(script) {
998
+ const tokens = [];
999
+ let current = "";
1000
+ let quote;
1001
+ let open = false;
1002
+ for (const char of script) {
1003
+ if (quote !== undefined) {
1004
+ if (char === quote)
1005
+ quote = undefined;
1006
+ else
1007
+ current += char;
1008
+ continue;
1009
+ }
1010
+ if (char === '"' || char === "'") {
1011
+ quote = char;
1012
+ open = true;
1013
+ continue;
1014
+ }
1015
+ if (SH_WHITESPACE.test(char)) {
1016
+ if (open)
1017
+ tokens.push(current);
1018
+ current = "";
1019
+ open = false;
1020
+ continue;
1021
+ }
1022
+ current += char;
1023
+ open = true;
1024
+ }
1025
+ if (open)
1026
+ tokens.push(current);
1027
+ return tokens;
1028
+ }
1029
+ function hostFlagOf(script) {
1030
+ const tokens = shellTokens(script);
1031
+ const spec = tokens.findIndex((token) => token.includes("@malloy-publisher/server"));
1032
+ if (spec === -1) {
1033
+ return;
1034
+ }
1035
+ let value;
1036
+ for (let i = spec + 1;i < tokens.length; i++) {
1037
+ const token = tokens[i];
1038
+ const next = tokens[i + 1];
1039
+ if (!SERVER_VALUE_FLAGS.has(token) || !next) {
1040
+ continue;
1041
+ }
1042
+ if (token === "--host") {
1043
+ value = next;
1044
+ }
1045
+ i++;
1046
+ }
1047
+ return value;
1048
+ }
1049
+ function joinedHostFlagOf(script) {
1050
+ let value;
1051
+ for (const match of script.matchAll(/(?:^|\s)--host=("[^"]*"|'[^']*'|\S*)/g)) {
1052
+ value = match[1].replace(/^["']|["']$/g, "");
1053
+ }
1054
+ return value;
1055
+ }
1056
+ function isLoopbackHost(host) {
1057
+ if (host === undefined) {
1058
+ return false;
1059
+ }
1060
+ const bare = host.replace(/^\[|\]$/g, "").toLowerCase();
1061
+ return bare === "localhost" || bare === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(bare);
1062
+ }
1063
+ function readMergeablePackageJson(cwd) {
1064
+ const manifest = readWorkspacePackageJson(cwd);
1065
+ if (manifest.kind === "absent") {
1066
+ return;
1067
+ }
1068
+ if (manifest.kind === "unusable") {
1069
+ throw new ScaffoldError(`package.json in ${cwd} is ${manifest.problem}. --force adds the ` + `Publisher scripts to an existing manifest and keeps everything ` + `else, which needs the file to be a JSON object this tool can read. ` + `Nothing was written. Fix or move it, then run again.`);
1070
+ }
1071
+ return manifest.pkg;
1072
+ }
1073
+ function writeWorkspacePackageJson(cwd, options, result) {
1074
+ const relPath = "package.json";
1075
+ const target = path3.join(cwd, relPath);
1076
+ const manifest = readWorkspacePackageJson(cwd);
1077
+ if (manifest.kind === "absent") {
1078
+ writeFile(target, workspacePackageJson(cwd, result), cwd);
1079
+ result.written.push(relPath);
1080
+ return;
1081
+ }
1082
+ if (manifest.kind === "unusable") {
1083
+ result.packageJsonProblem = manifest.problem;
1084
+ result.skipped.push(`${relPath} (${manifest.problem}, so the Publisher start script was ` + `not added)`);
1085
+ return;
1086
+ }
1087
+ if (!options.force) {
1088
+ result.skipped.push(relPath);
1089
+ return;
1090
+ }
1091
+ const pkg = manifest.pkg;
1092
+ const scripts = asJsonObject(pkg.scripts) ?? {};
1093
+ pkg.scripts = {
1094
+ ...scripts,
1095
+ start: result.startCommand,
1096
+ reset: result.resetCommand
1097
+ };
1098
+ writeFile(target, JSON.stringify(pkg, null, 2) + `
1099
+ `, cwd);
1100
+ result.written.push(`${relPath} (start and reset scripts set)`);
1101
+ for (const name of ["start", "reset"]) {
1102
+ const previous = scripts[name];
1103
+ if (typeof previous === "string" && !isPublisherScript(previous, result.envName, name === "reset")) {
1104
+ result.replaced.push(`${relPath} ${name} script ("${printable(previous)}")`);
1105
+ }
1106
+ }
1107
+ }
1108
+ function writeMcpConfig(cwd, relPath, result) {
1109
+ const target = path3.join(cwd, relPath);
1110
+ if (!fs4.existsSync(target)) {
1111
+ writeFile(target, mcpConfig(), cwd);
1112
+ result.written.push(relPath);
1113
+ result.mcpWired = true;
1114
+ return;
1115
+ }
1116
+ let parsed;
1117
+ let parses = true;
1118
+ try {
1119
+ parsed = parseJson(fs4.readFileSync(target, "utf8"));
1120
+ } catch {
1121
+ parses = false;
1122
+ }
1123
+ const config = asJsonObject(parsed);
1124
+ const servers = config && "mcpServers" in config ? asJsonObject(config.mcpServers) : undefined;
1125
+ if (!config || config.mcpServers !== undefined && !servers) {
1126
+ const problem = describeMcpProblem(parses, parsed, config);
1127
+ result.skipped.push(`${relPath} (${problem})`);
1128
+ result.mcpWired = false;
1129
+ result.mcpConfigProblem = problem;
1130
+ result.mcpPasteBlock = mcpConfig();
1131
+ return;
1132
+ }
1133
+ const existingEntry = servers?.malloy;
1134
+ const desired = malloyServer();
1135
+ result.mcpWired = true;
1136
+ result.mcpOtherServers = Object.keys(servers ?? {}).filter((name) => name !== "malloy").length;
1137
+ if (JSON.stringify(existingEntry) === JSON.stringify(desired)) {
1138
+ result.skipped.push(`${relPath} (malloy already wired)`);
1139
+ return;
1140
+ }
1141
+ result.mcpEntryReplaced = existingEntry !== undefined;
1142
+ config.mcpServers = { ...servers ?? {}, malloy: desired };
1143
+ result.mcpMerged = true;
1144
+ writeFile(target, JSON.stringify(config, null, 2) + `
1145
+ `, cwd);
1146
+ result.written.push(`${relPath} (${existingEntry === undefined ? "added" : "updated"} the malloy server)`);
1147
+ if (existingEntry !== undefined) {
1148
+ result.replaced.push(`${relPath} malloy server`);
1149
+ }
1150
+ }
1151
+ function describeMcpProblem(parses, parsed, config) {
1152
+ if (!parses) {
1153
+ return "not valid JSON";
1154
+ }
1155
+ if (!config) {
1156
+ return `valid JSON, but ${describeJsonValue(parsed)} rather than a JSON object`;
1157
+ }
1158
+ return `valid JSON, but its "mcpServers" is ${describeJsonValue(config.mcpServers)} rather than a JSON object`;
1159
+ }
1160
+ function renderAgentsFile(result, host, envPackages) {
1161
+ const mcpNote = !result.mcpWired ? `\`${result.mcpConfigPath}\` could not be updated automatically, so the \`malloy\` server may still need adding to it by hand.` : host === "cursor" ? "This workspace ships a `.cursor/mcp.json` that points Cursor at the endpoint." : "This workspace ships a `.mcp.json`, so Claude Code offers to connect on first run.";
1162
+ const reconnectNote = host === "cursor" ? "ask the user to reload the MCP servers from Cursor's settings (the `malloy` server, then Refresh), or to restart Cursor." : "ask the user to run `/mcp`, select the `malloy` server, and choose Reconnect, or to restart Claude Code. That panel reports `Auth: not authenticated` and offers `Authenticate` first, which is a red herring: this endpoint has no auth, and Reconnect is the one that works.";
1163
+ const skillsCount = String(result.skillsInstalled);
1164
+ const skillsNote = result.skillsInstalled === 0 ? "This run installed no skills into `.claude/skills/` (the scaffolder's output says why), so there are none here to load. Pull the same guidance as MCP prompts from the endpoint above instead." : host === "cursor" ? `\`.claude/skills/\` holds ${skillsCount} Malloy agent skills as real files. Cursor reads \`AGENTS.md\`; these skills are the same guidance broken out by task, and any MCP client can also pull them as prompts from the endpoint above.` : `\`.claude/skills/\` holds ${skillsCount} Malloy agent skills as real files, so Claude Code auto-discovers them. Hosts that read \`AGENTS.md\` rather than Anthropic Agent Skills can pull the same guidance as MCP prompts from the endpoint above.`;
1165
+ const startCommand = result.hasStartScript ? "npm start" : result.startCommand;
1166
+ const portOverrideCommand = `${startCommand}${result.hasStartScript ? " --" : ""} ` + `--port ${ALT_PUBLISHER_PORT} --mcp_port ${ALT_MCP_PORT}`;
1167
+ return renderTemplate("AGENTS.md", {
1168
+ title: result.packageCreated ? `${result.packageName}: a Malloy Publisher package` : "A Malloy Publisher workspace",
1169
+ startCommand,
1170
+ portOverrideCommand,
1171
+ resetCommand: result.hasResetScript ? "npm run reset" : result.resetCommand,
1172
+ skillsCount,
1173
+ envName: result.envName,
1174
+ port: String(result.publisherPort),
1175
+ mcpPort: String(result.mcpPort),
1176
+ packageSection: packageSection(result, envPackages),
1177
+ mcpNote,
1178
+ reconnectNote,
1179
+ skillsNote
1180
+ });
1181
+ }
1182
+ function packageSection(result, envPackages) {
1183
+ const others = envPackages.filter((name) => name !== result.packageName);
1184
+ if (!result.packageCreated && others.length === 0) {
1185
+ return "";
1186
+ }
1187
+ const restBase = (name) => `http://localhost:${result.publisherPort}/api/v0/environments/` + `${result.envName}/packages/${name}`;
1188
+ const lines = [
1189
+ "",
1190
+ others.length > 0 ? "## The packages" : "## The package",
1191
+ ""
1192
+ ];
1193
+ if (result.packageCreated) {
1194
+ const base = restBase(result.packageName);
1195
+ lines.push(`\`${result.packageName}/${result.modelFile}\` defines a Malloy source named \`${result.sourceName}\` over local data. Read it for the real source, field, and view names and use them verbatim; never guess them. The package's REST base is:`, "", "```", base, "```", "", "Run one of its views from a script:", "", "```bash", `curl -s -X POST ${base}/models/${result.modelFile}/query \\`, " -H 'content-type: application/json' \\", ` -d '{"query":"run: ${result.sourceName} -> overview"}'`, "```", "");
1196
+ }
1197
+ if (others.length > 0) {
1198
+ lines.push(otherPackagesParagraph(result, others, restBase), "");
1199
+ }
1200
+ return lines.join(`
1201
+ `);
1202
+ }
1203
+ function otherPackagesParagraph(result, others, restBase) {
1204
+ const plural = others.length === 1 ? "" : "s";
1205
+ const opening = result.packageCreated ? `This file is regenerated on every run and details only the package that run created. \`publisher.config.json\` registers ${others.length} other package${plural} in the \`${result.envName}\` environment` : `This run created no package. \`publisher.config.json\` registers ${others.length} package${plural} in the \`${result.envName}\` environment`;
1206
+ const named = others.every(isNameableHere) ? `: ${others.map((name) => `\`${name}\``).join(", ")}` : `, whose names this file does not repeat because at least one of them is outside the character set Publisher accepts in a URL`;
1207
+ const shape = result.packageCreated ? `the one above with the package name changed` : `\`${restBase("<package>")}\``;
1208
+ return `${opening}${named}. Nothing here has read those models, so use ` + `\`malloy_getContext\`, or read the \`.malloy\` file in each package's ` + `own directory, for its source, field, and view names. Their REST bases ` + `are ${shape}. The packages endpoint further down lists what the running ` + `server actually mounted, which is the only answer that counts.`;
1209
+ }
1210
+ function isNameableHere(name) {
1211
+ try {
1212
+ validatePackageName(name);
1213
+ return true;
1214
+ } catch {
1215
+ return false;
1216
+ }
1217
+ }
1218
+ function workspacePackageJson(cwd, result) {
1219
+ return JSON.stringify({
1220
+ name: toNpmName(path3.basename(path3.resolve(cwd))),
1221
+ version: "0.1.0",
1222
+ private: true,
1223
+ scripts: {
1224
+ start: result.startCommand,
1225
+ reset: result.resetCommand
1226
+ }
1227
+ }, null, 2) + `
1228
+ `;
1229
+ }
1230
+ var GITIGNORE_HEADER = "# Malloy Publisher";
1231
+ function workspaceGitignoreEntries() {
1232
+ return [
1233
+ "node_modules/",
1234
+ "publisher_data/",
1235
+ "publisher.db*",
1236
+ "*.log",
1237
+ ".DS_Store"
1238
+ ];
1239
+ }
1240
+ function workspaceGitignore() {
1241
+ return [GITIGNORE_HEADER, ...workspaceGitignoreEntries(), ""].join(`
1242
+ `);
1243
+ }
1244
+ function mergeGitignore(cwd, result) {
1245
+ const relPath = ".gitignore";
1246
+ const target = path3.join(cwd, relPath);
1247
+ if (!fs4.existsSync(target)) {
1248
+ writeFile(target, workspaceGitignore(), cwd);
1249
+ result.written.push(relPath);
1250
+ return;
1251
+ }
1252
+ assertWithinWorkspace(target, cwd, relPath);
1253
+ let existing;
1254
+ try {
1255
+ existing = fs4.readFileSync(target, "utf8");
1256
+ } catch (err) {
1257
+ result.skipped.push(`${relPath} (could not be read, so Publisher's files are not ` + `ignored here: ${err.message})`);
1258
+ return;
1259
+ }
1260
+ const present = new Set(existing.split(/\r?\n/).map(normalizeIgnoreEntry).filter(Boolean));
1261
+ const missing = workspaceGitignoreEntries().filter((entry) => !present.has(normalizeIgnoreEntry(entry)));
1262
+ if (missing.length === 0) {
1263
+ result.skipped.push(`${relPath} (already ignores what Publisher writes)`);
1264
+ return;
1265
+ }
1266
+ const prefix = existing === "" ? "" : existing.endsWith(`
1267
+ `) ? `
1268
+ ` : `
1269
+
1270
+ `;
1271
+ fs4.appendFileSync(target, `${prefix}${GITIGNORE_HEADER}
1272
+ ${missing.join(`
1273
+ `)}
1274
+ `);
1275
+ const entries = missing.length === 1 ? "entry" : "entries";
1276
+ result.written.push(`${relPath} (${missing.length} ${entries} appended, nothing removed)`);
1277
+ }
1278
+ function normalizeIgnoreEntry(line) {
1279
+ const trimmed = line.trim();
1280
+ if (trimmed === "" || trimmed.startsWith("#")) {
1281
+ return "";
1282
+ }
1283
+ return trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
1284
+ }
1285
+ function malloyServer() {
1286
+ return { type: "http", url: `http://localhost:${MCP_PORT}/mcp` };
1287
+ }
1288
+ function mcpConfig() {
1289
+ return JSON.stringify({ mcpServers: { malloy: malloyServer() } }, null, 2) + `
1290
+ `;
1291
+ }
1292
+ function claudeFile(agentsFile) {
1293
+ const pointer = `See @${agentsFile} for how to start Publisher, connect an agent to the ` + `MCP endpoint, and use the bundled Malloy skills.
1294
+ `;
1295
+ if (agentsFile === "AGENTS.md") {
1296
+ return pointer;
1297
+ }
1298
+ return pointer + `
1299
+ This directory already had an AGENTS.md when the Malloy workspace was ` + `scaffolded, so that file was left exactly as it was and carries none of ` + `the above. ${agentsFile} is the generated briefing; read that one for ` + `anything about Publisher, Malloy, or the MCP endpoint.
1300
+ `;
1301
+ }
1302
+ function validateDataFile(dataFile) {
1303
+ if (!fs4.existsSync(dataFile) || !fs4.statSync(dataFile).isFile()) {
1304
+ throw new ScaffoldError(`--data file not found: ${printable(dataFile)}`);
1305
+ }
1306
+ const ext = path3.extname(dataFile).toLowerCase();
1307
+ if (ext !== ".csv" && ext !== ".parquet" && ext !== ".xlsx") {
1308
+ throw new ScaffoldError(`--data must be a .csv, .parquet, or .xlsx file (got "${printable(path3.basename(dataFile))}").`);
1309
+ }
1310
+ }
1311
+ function toNpmName(base) {
1312
+ const cleaned = base.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^[._]+/, "");
1313
+ return cleaned || "malloy-workspace";
1314
+ }
1315
+ function sanitizeFileName(base) {
1316
+ return base.replace(/[^A-Za-z0-9._-]/g, "_");
1317
+ }
1318
+ function writeIfAbsent(cwd, relPath, content, options, result) {
1319
+ const target = path3.join(cwd, relPath);
1320
+ const exists = fs4.existsSync(target);
1321
+ if (exists && !options.force) {
1322
+ result.skipped.push(relPath);
1323
+ return false;
1324
+ }
1325
+ writeFile(target, content, cwd);
1326
+ result.written.push(relPath);
1327
+ if (exists) {
1328
+ result.replaced.push(relPath);
1329
+ }
1330
+ return true;
1331
+ }
1332
+ function writeFile(target, content, root) {
1333
+ assertWithinWorkspace(target, root, path3.relative(root, target) || target);
1334
+ fs4.mkdirSync(path3.dirname(target), { recursive: true });
1335
+ fs4.writeFileSync(target, content);
1336
+ }
1337
+ function copyFile(source, target, root, label) {
1338
+ assertWithinWorkspace(target, root, label);
1339
+ fs4.copyFileSync(source, target);
1340
+ }
1341
+
1342
+ // src/index.ts
1343
+ var CLIENTS = ["claude-code", "cursor"];
1344
+ function version() {
1345
+ try {
1346
+ const pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
1347
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
1348
+ } catch {
1349
+ return "unknown";
1350
+ }
1351
+ }
1352
+ function asClient(value) {
1353
+ return value === "cursor" ? "cursor" : "claude-code";
1354
+ }
1355
+ function resolveDataFile(cwd, data) {
1356
+ if (data === undefined) {
1357
+ return;
1358
+ }
1359
+ if (data.trim() === "") {
1360
+ throw new ScaffoldError(`--data was given an empty filename. Pass the CSV, Parquet, or XLSX ` + `file to seed the package from, or leave --data off to use the ` + `sample data.`);
1361
+ }
1362
+ return path4.resolve(cwd, data);
1363
+ }
1364
+ function run(name, options) {
1365
+ const cwd = process.cwd();
1366
+ const before = snapshotArtifacts(cwd, name);
1367
+ try {
1368
+ const result = scaffold({
1369
+ name,
1370
+ cwd,
1371
+ dataFile: resolveDataFile(cwd, options.data),
1372
+ host: asClient(options.client),
1373
+ force: Boolean(options.force)
1374
+ });
1375
+ process.stdout.write(formatSuccess(result));
1376
+ } catch (err) {
1377
+ process.stderr.write(formatFailure(err, changesSince(cwd, name, before), Boolean(options.force)));
1378
+ process.exitCode = 1;
1379
+ }
1380
+ }
1381
+ function artifactCandidates(name) {
1382
+ const paths = [
1383
+ "publisher.config.json",
1384
+ "package.json",
1385
+ ".mcp.json",
1386
+ path4.join(".cursor", "mcp.json"),
1387
+ ".gitignore",
1388
+ "AGENTS.md",
1389
+ "CLAUDE.md",
1390
+ path4.join(".claude", "skills")
1391
+ ];
1392
+ if (name !== undefined && name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\")) {
1393
+ paths.unshift(name);
1394
+ }
1395
+ return paths;
1396
+ }
1397
+ function snapshotArtifacts(cwd, name) {
1398
+ const snapshot = new Map;
1399
+ for (const relPath of artifactCandidates(name)) {
1400
+ snapshot.set(relPath, fingerprint(cwd, relPath));
1401
+ }
1402
+ return snapshot;
1403
+ }
1404
+ function changesSince(cwd, name, before) {
1405
+ const changes = { created: [], changed: [] };
1406
+ for (const relPath of artifactCandidates(name)) {
1407
+ const was = before.get(relPath);
1408
+ const now = fingerprint(cwd, relPath);
1409
+ if (now === undefined || now === was) {
1410
+ continue;
1411
+ }
1412
+ if (was === undefined) {
1413
+ changes.created.push(relPath);
1414
+ } else {
1415
+ changes.changed.push(relPath);
1416
+ }
1417
+ }
1418
+ return changes;
1419
+ }
1420
+ function fingerprint(cwd, relPath) {
1421
+ const target = path4.join(cwd, relPath);
1422
+ let stat;
1423
+ try {
1424
+ stat = fs5.lstatSync(target);
1425
+ } catch {
1426
+ return;
1427
+ }
1428
+ try {
1429
+ if (stat.isDirectory()) {
1430
+ return `dir:${digest(listDirectory(target))}`;
1431
+ }
1432
+ if (stat.isFile()) {
1433
+ return `file:${digest(fs5.readFileSync(target))}`;
1434
+ }
1435
+ return `other:${stat.mode}:${linkTarget(target)}`;
1436
+ } catch {
1437
+ return "unreadable";
1438
+ }
1439
+ }
1440
+ function listDirectory(dir, cap = 5000) {
1441
+ const entries = [];
1442
+ const stack = [dir];
1443
+ while (stack.length > 0 && entries.length < cap) {
1444
+ const current = stack.pop();
1445
+ let children;
1446
+ try {
1447
+ children = fs5.readdirSync(current, { withFileTypes: true });
1448
+ } catch {
1449
+ entries.push(`${path4.relative(dir, current)} unreadable`);
1450
+ continue;
1451
+ }
1452
+ for (const child of children) {
1453
+ const full = path4.join(current, child.name);
1454
+ if (child.isDirectory()) {
1455
+ stack.push(full);
1456
+ continue;
1457
+ }
1458
+ let size = -1;
1459
+ let mtime = -1;
1460
+ try {
1461
+ const childStat = fs5.lstatSync(full);
1462
+ size = childStat.size;
1463
+ mtime = childStat.mtimeMs;
1464
+ } catch {}
1465
+ entries.push(`${path4.relative(dir, full)} ${size} ${mtime}`);
1466
+ }
1467
+ }
1468
+ entries.sort();
1469
+ return entries.join(`
1470
+ `);
1471
+ }
1472
+ function linkTarget(target) {
1473
+ try {
1474
+ return fs5.readlinkSync(target);
1475
+ } catch {
1476
+ return "";
1477
+ }
1478
+ }
1479
+ function digest(data) {
1480
+ return crypto.createHash("sha1").update(data).digest("hex");
1481
+ }
1482
+ function formatFailure(err, changes, force) {
1483
+ const lines = ["", `${red("Error:")} ${describeFailure(err)}`];
1484
+ if (changes.created.length > 0) {
1485
+ lines.push("");
1486
+ lines.push(yellow("This run created these before it stopped:"));
1487
+ for (const relPath of changes.created) {
1488
+ lines.push(` ${relPath}`);
1489
+ }
1490
+ }
1491
+ if (changes.changed.length > 0) {
1492
+ lines.push("");
1493
+ lines.push(yellow("It also rewrote these, which were already here:"));
1494
+ for (const relPath of changes.changed) {
1495
+ lines.push(` ${relPath}`);
1496
+ }
1497
+ }
1498
+ if (changes.created.length > 0 || changes.changed.length > 0) {
1499
+ lines.push(dim(` Both lists come from comparing the paths this tool writes
1500
+ ` + " against this directory as it was when the run started."));
1501
+ lines.push(dim(force ? ` Fix the cause and run again: --force is already set, so the
1502
+ ` + " next run writes over what is listed above." : ` Remove what it created before running again, or re-run with
1503
+ ` + " --force once the cause is fixed."));
1504
+ }
1505
+ const stack = err instanceof Error ? err.stack : undefined;
1506
+ if (stack && !(err instanceof ScaffoldError) && !isErrno(err)) {
1507
+ lines.push("");
1508
+ lines.push(dim(" This looks like a bug in create-malloy-package. Re-run with " + "DEBUG=1 for the stack trace."));
1509
+ if (process.env.DEBUG) {
1510
+ lines.push("");
1511
+ lines.push(dim(stack));
1512
+ }
1513
+ }
1514
+ lines.push("");
1515
+ return lines.join(`
1516
+ `) + `
1517
+ `;
1518
+ }
1519
+ function isErrno(err) {
1520
+ return err instanceof Error && typeof err.code === "string" && /^E[A-Z]+$/.test(err.code);
1521
+ }
1522
+ var ERRNO_REASON = {
1523
+ EACCES: "permission denied",
1524
+ EEXIST: "it already exists",
1525
+ EISDIR: "it is a directory, not a file",
1526
+ ELOOP: "too many symbolic links to follow",
1527
+ EMFILE: "too many open files",
1528
+ ENAMETOOLONG: "the name is longer than this filesystem allows",
1529
+ ENOENT: "no such file or directory",
1530
+ ENOSPC: "there is no space left on the device",
1531
+ ENOTDIR: "something along that path is a regular file, so it cannot hold a directory",
1532
+ ENOTEMPTY: "the directory is not empty",
1533
+ EPERM: "the operation is not permitted",
1534
+ EROFS: "the filesystem is read-only"
1535
+ };
1536
+ function describeFailure(err) {
1537
+ if (err instanceof ScaffoldError) {
1538
+ return err.message;
1539
+ }
1540
+ if (isErrno(err)) {
1541
+ const reason = ERRNO_REASON[err.code];
1542
+ const target = err.path ?? err.dest;
1543
+ const what = err.syscall ? `Could not ${err.syscall}` : "Failed";
1544
+ const where = target ? ` ${relativeToCwd(target)}` : "";
1545
+ return reason ? `${what}${where}: ${reason} (${err.code}).` : `${what}${where}: ${err.message}`;
1546
+ }
1547
+ if (err instanceof Error) {
1548
+ return err.message;
1549
+ }
1550
+ return String(err);
1551
+ }
1552
+ function relativeToCwd(target) {
1553
+ const relative3 = path4.relative(process.cwd(), target);
1554
+ return relative3 && !relative3.startsWith("..") && !path4.isAbsolute(relative3) ? `./${relative3}` : target;
1555
+ }
1556
+ function readWorkspaceFile(cwd, relPath) {
1557
+ try {
1558
+ return fs5.readFileSync(path4.join(cwd, relPath), "utf8");
1559
+ } catch {
1560
+ return;
1561
+ }
1562
+ }
1563
+ function agentsFileNotes(result) {
1564
+ if (!result.skipped.includes("AGENTS.md") || result.agentsFile === "AGENTS.md") {
1565
+ return [];
1566
+ }
1567
+ const wroteClaudeFile = result.written.includes("CLAUDE.md") || result.replaced.includes("CLAUDE.md");
1568
+ if (!wroteClaudeFile) {
1569
+ return [
1570
+ dim(` That AGENTS.md is the project's own, so it carries no Publisher
1571
+ ` + ` instructions and none were added to it. This run's briefing ` + `is in
1572
+ ${result.agentsFile} instead.`)
1573
+ ];
1574
+ }
1575
+ const claude = readWorkspaceFile(result.cwd, "CLAUDE.md") ?? "";
1576
+ if (claude.includes(`@${result.agentsFile}`)) {
1577
+ return [
1578
+ dim(` That AGENTS.md is the project's own, so it carries no Publisher
1579
+ ` + ` instructions and none were added to it. This run's briefing ` + `is in
1580
+ ${result.agentsFile} instead, and the CLAUDE.md this ` + `run wrote points there.`)
1581
+ ];
1582
+ }
1583
+ return [
1584
+ yellow(` That AGENTS.md is the project's own: no start command, no ports, no
1585
+ ` + ` MCP endpoint, no skills index. The briefing went to ` + `${result.agentsFile},
1586
+ but the CLAUDE.md this run wrote does not ` + `point at that file, and
1587
+ Claude Code reads CLAUDE.md on its own. ` + `Point it at ${result.agentsFile}
1588
+ by hand.`)
1589
+ ];
1590
+ }
1591
+ function declinedScriptReason(declined, kind) {
1592
+ const noneAdded = `no Publisher ${kind} script was added to this directory's package.json`;
1593
+ if (declined === undefined) {
1594
+ return noneAdded;
1595
+ }
1596
+ switch (declined.reason) {
1597
+ case "no-host-flag":
1598
+ return declined.joinedHostFlag === undefined ? `this directory's ${kind} script boots Publisher with no --host, ` + "and Publisher binds every interface by default" : `this directory's ${kind} script writes ` + `--host=${printable(declined.joinedHostFlag)}, which ` + "Publisher's own argument parser does not accept: it reads " + "--host and the address as two arguments, drops the joined " + "form without a word, and binds every interface";
1599
+ case "unrecognised-shape":
1600
+ return `this directory's ${kind} script names the Publisher server inside ` + "a longer command line, so what it runs, and what it binds, is not " + "something this tool established";
1601
+ case "unmodelled-shell-syntax":
1602
+ return `this directory's ${kind} script uses shell syntax this tool does ` + "not read (a comment, an escape, or a substitution), so what the " + "server is handed, and what it binds, is not something this tool " + "established";
1603
+ case "unparseable":
1604
+ return `this directory's ${kind} script leaves a quote open, so the shell ` + "refuses to run it at all and it starts nothing";
1605
+ case "different-invocation":
1606
+ return `this directory's ${kind} script boots Publisher with different ` + "arguments to the ones here, so the ports and the workspace root " + "below are not what it would serve";
1607
+ case "non-loopback-host":
1608
+ return `this directory's ${kind} script boots Publisher on ` + `${printable(String(declined.host))}, which is not a loopback address`;
1609
+ case "no-init-flag":
1610
+ return `this directory's ${kind} script boots Publisher but carries no ` + "--init, so it would not rebuild the package list from the config";
1611
+ case "not-publisher": {
1612
+ const script = declined.script.trim();
1613
+ return script === "" ? noneAdded : `this directory's ${kind} script runs something else: ` + `\`${preview(script)}\`, so it was left alone`;
1614
+ }
1615
+ default:
1616
+ return `this directory's ${kind} script is not one this tool recognises ` + "as its own Publisher boot";
1617
+ }
1618
+ }
1619
+ function bindRisk(declined) {
1620
+ if (declined === undefined) {
1621
+ return;
1622
+ }
1623
+ switch (declined.reason) {
1624
+ case "not-publisher":
1625
+ return;
1626
+ case "no-init-flag":
1627
+ case "different-invocation":
1628
+ return;
1629
+ case "no-host-flag":
1630
+ return { known: true, address: "0.0.0.0, Publisher's default" };
1631
+ case "non-loopback-host":
1632
+ return declined.host === undefined ? { known: false } : { known: true, address: printable(declined.host) };
1633
+ case "unparseable":
1634
+ return;
1635
+ case "unrecognised-shape":
1636
+ case "unmodelled-shell-syntax":
1637
+ return { known: false };
1638
+ default:
1639
+ return { known: false };
1640
+ }
1641
+ }
1642
+ function exposureWarning(command, declined) {
1643
+ const risk = bindRisk(declined);
1644
+ if (risk === undefined) {
1645
+ return [];
1646
+ }
1647
+ return [
1648
+ yellow(risk.known ? ` Do not use \`${command}\` here: that script binds ${risk.address},
1649
+ ` + ` which serves the unauthenticated REST API and MCP endpoint ` + `to your
1650
+ whole network. The command above binds 127.0.0.1.` : ` Do not use \`${command}\` here: this tool could not establish ` + `what
1651
+ that script binds, and Publisher serves an ` + `unauthenticated REST API
1652
+ and MCP endpoint on whatever it ` + `binds. The command above binds
1653
+ 127.0.0.1.`)
1654
+ ];
1655
+ }
1656
+ function wroteStartScript(result) {
1657
+ return result.written.some((item) => item === "package.json" || item.startsWith("package.json "));
1658
+ }
1659
+ function formatSuccess(result) {
1660
+ const lines = [""];
1661
+ if (result.packageCreated) {
1662
+ lines.push(`${green("✓")} Created package ${bold(result.packageName)} in ./${result.packageName}`);
1663
+ }
1664
+ if (result.dataFileRenamedFrom) {
1665
+ lines.push(dim(` ${result.dataFileRenamedFrom} was copied in as ${result.dataPath}, ` + `renamed to the characters a Malloy table path can carry.`));
1666
+ }
1667
+ if (result.configExtended) {
1668
+ lines.push(`${green("✓")} Registered ${bold(result.packageName)} in the existing publisher.config.json`);
1669
+ }
1670
+ if (result.mcpWired) {
1671
+ lines.push(`${green("✓")} Wired the agent workspace (${result.host}) in ${result.cwd}`);
1672
+ } else {
1673
+ lines.push(`${yellow("!")} Set up the agent workspace (${result.host}) in ${result.cwd}, but the MCP endpoint is not wired`);
1674
+ }
1675
+ if (result.skillsInstalled > 0) {
1676
+ lines.push(`${green("✓")} Installed ${bold(String(result.skillsInstalled))} Malloy skills in .claude/skills/`);
1677
+ } else {
1678
+ lines.push(yellow("!") + " No Malloy skills were installed in .claude/skills/");
1679
+ }
1680
+ if (result.written.length > 0) {
1681
+ lines.push("");
1682
+ lines.push(bold("Wrote these:"));
1683
+ for (const item of result.written) {
1684
+ lines.push(` ${printable(item)}`);
1685
+ }
1686
+ }
1687
+ if (result.skipped.length > 0) {
1688
+ lines.push("");
1689
+ lines.push(yellow("Left these existing files alone:"));
1690
+ for (const item of result.skipped) {
1691
+ lines.push(` ${printable(item)}`);
1692
+ }
1693
+ lines.push(...agentsFileNotes(result));
1694
+ }
1695
+ if (result.replaced.length > 0) {
1696
+ lines.push("");
1697
+ lines.push(yellow("Overwrote these:"));
1698
+ for (const item of result.replaced) {
1699
+ lines.push(` ${printable(item)}`);
1700
+ }
1701
+ }
1702
+ const url = `http://localhost:${result.publisherPort}`;
1703
+ const environmentIsEmpty = result.envPackageCount === 0;
1704
+ lines.push("");
1705
+ lines.push(bold("Next steps:"));
1706
+ if (environmentIsEmpty) {
1707
+ lines.push(` ${cyan("npx @malloy-publisher/create-malloy-package <name>")} ${dim("add a package; the server has nothing to serve without one")}`);
1708
+ }
1709
+ if (result.needsReset) {
1710
+ const served = `re-read the config, so ${result.packageName ?? "the new package"} is served`;
1711
+ if (result.hasResetScript) {
1712
+ lines.push(` ${cyan("npm run reset")} ${dim(served)}`);
1713
+ } else {
1714
+ lines.push(` ${dim(`${served} (${declinedScriptReason(result.declinedResetScript, "reset")}):`)}`);
1715
+ lines.push(` ${cyan(result.resetCommand)}`);
1716
+ }
1717
+ lines.push(dim(` Restarting is not enough: Publisher reads publisher.config.json
1718
+ ` + ` once, and after that its persisted package list wins. This boot
1719
+ ` + ` carries --init, which rebuilds the package list from the config
1720
+ ` + ` and drops the rest of the persisted state with it: materialized
1721
+ ` + ` tables, saved themes, and anything set through the API rather
1722
+ ` + " than the config file."));
1723
+ lines.push(dim(` Stop any Publisher already serving this directory first. One
1724
+ ` + ` server per workspace: a second one dies on the lock over
1725
+ ` + ` publisher.db, and moving it to free ports instead leaves it
1726
+ ` + ` at initializing forever, because the lock is on the
1727
+ ` + " workspace, not the port."));
1728
+ if (result.hasStartScript) {
1729
+ lines.push(` ${cyan("npm start")} ${dim("for every boot after that")}`);
1730
+ } else {
1731
+ lines.push(` ${dim("for every boot after that:")}`);
1732
+ lines.push(` ${cyan(result.startCommand)}`);
1733
+ }
1734
+ if (!result.hasResetScript) {
1735
+ lines.push(...exposureWarning("npm run reset", result.declinedResetScript));
1736
+ }
1737
+ } else if (result.hasStartScript) {
1738
+ lines.push(` ${cyan("npm start")} ${dim(startNote(result, environmentIsEmpty))}`);
1739
+ } else {
1740
+ lines.push(` ${dim(`start Publisher (${declinedScriptReason(result.declinedStartScript, "start")}):`)}`);
1741
+ lines.push(` ${cyan(result.startCommand)}`);
1742
+ }
1743
+ if (!result.hasStartScript) {
1744
+ lines.push(...exposureWarning("npm start", result.declinedStartScript));
1745
+ }
1746
+ lines.push(` ${cyan(url)} ${dim("explore in the browser")}`);
1747
+ lines.push("");
1748
+ lines.push(bold("Check it is ready:"));
1749
+ lines.push(` curl -s ${url}/api/v0/status`);
1750
+ const packagesUrl = `${url}/api/v0/environments/${result.envName}/packages`;
1751
+ if (environmentIsEmpty) {
1752
+ lines.push(dim(` It reports serving with nothing to serve. There is no
1753
+ ` + ` environment to list packages from until a package exists, so
1754
+ ` + ` ${packagesUrl}
1755
+ ` + " answers 404 until then."));
1756
+ } else {
1757
+ lines.push(` curl -s ${packagesUrl}`);
1758
+ lines.push(dim(` The second one is the check that matters: the status says
1759
+ ` + ` serving even when nothing loaded. A package the server could
1760
+ ` + " not mount is simply missing from that list, so " + (result.packageCreated ? `an empty []
1761
+ means it could not load ${result.packageName}, and the reason is in the
1762
+ server's own log.` : `a package
1763
+ missing from it is one the server could not load, and the reason is
1764
+ in the server's own log.`)));
1765
+ }
1766
+ lines.push("");
1767
+ lines.push(bold("Connect an agent:"));
1768
+ lines.push(` MCP endpoint http://localhost:${result.mcpPort}/mcp`);
1769
+ if (result.mcpMerged) {
1770
+ const added = !result.mcpEntryReplaced;
1771
+ const others = result.mcpOtherServers;
1772
+ const alongside = others > 0 ? `, alongside the ${others} server${others === 1 ? "" : "s"} already in it` : "";
1773
+ lines.push(dim(added ? ` the malloy server was added to your ${result.mcpConfigPath}${alongside}.` : ` your ${result.mcpConfigPath} already had a malloy server pointing ` + `somewhere else; it now points at the endpoint above${alongside}.`));
1774
+ } else if (result.mcpWired) {
1775
+ lines.push(dim(` ${result.mcpConfigPath} points at it${result.host === "cursor" ? "" : ", so Claude Code offers it"}.`));
1776
+ } else {
1777
+ const problem = result.mcpConfigProblem ?? "not usable";
1778
+ const paste = (result.mcpPasteBlock ?? "").trimEnd();
1779
+ if (paste === "") {
1780
+ lines.push(yellow(` ${result.mcpConfigPath} is ${problem}, so it was left alone. ` + `Add an MCP server named "malloy" pointing at the endpoint ` + `above by hand; ${result.agentsFile} has the block.`));
1781
+ } else {
1782
+ lines.push(yellow(` ${result.mcpConfigPath} is ${problem}, so it was left alone. ` + `Add this server to it by hand:`));
1783
+ for (const line of paste.split(`
1784
+ `)) {
1785
+ lines.push(` ${line}`);
1786
+ }
1787
+ }
1788
+ }
1789
+ lines.push(dim(` An agent that starts the server itself can't reconnect MCP in that
1790
+ ` + ` session; ask the user to reconnect it. See ${result.agentsFile}.`));
1791
+ lines.push("");
1792
+ return lines.join(`
1793
+ `) + `
1794
+ `;
1795
+ }
1796
+ function startNote(result, environmentIsEmpty) {
1797
+ if (!wroteStartScript(result)) {
1798
+ return environmentIsEmpty ? "run this directory's own start script; it boots Publisher on a loopback address, with no environment to browse" : "run this directory's own start script, which boots Publisher on a loopback address";
1799
+ }
1800
+ if (environmentIsEmpty) {
1801
+ return "start Publisher anyway; it reports serving, with no environment to browse";
1802
+ }
1803
+ if (result.packageCreated) {
1804
+ return "start Publisher (web UI + MCP) with the package in watch mode";
1805
+ }
1806
+ return "start Publisher (web UI + MCP) with this workspace's packages in watch mode";
1807
+ }
1808
+ function excessArgumentsHint() {
1809
+ const npmCreateFix = ` ${cyan("npm create @malloy-publisher/malloy-package sales -- --data mydata.csv")}`;
1810
+ if (process.env.npm_command === "init") {
1811
+ return [
1812
+ "",
1813
+ "npm parsed this command line before the scaffolder saw it, so the",
1814
+ "options came through as bare arguments. Put a -- in front of them:",
1815
+ npmCreateFix,
1816
+ ""
1817
+ ].join(`
1818
+ `);
1819
+ }
1820
+ return [
1821
+ "",
1822
+ "If you ran this through `npm create`, npm parsed the command line before",
1823
+ "the scaffolder saw it and the options came through as bare arguments.",
1824
+ "Put a -- in front of them:",
1825
+ npmCreateFix,
1826
+ "",
1827
+ "With npx there is no separator: it forwards the flags as they are, and a",
1828
+ "-- would be passed through and counted as another argument.",
1829
+ ` ${cyan("npx @malloy-publisher/create-malloy-package sales --data mydata.csv")}`,
1830
+ ""
1831
+ ].join(`
1832
+ `);
1833
+ }
1834
+ var program = new Command;
1835
+ program.name("create-malloy-package").description("Scaffold a Malloy Publisher package and a local agent workspace.").version(version(), "-v, --version").argument("[name]", "package name to create; omit to only wire the agent workspace into the current directory").option("--data <file>", "seed the package from a CSV, Parquet, or XLSX file").addOption(new Option("--client <client>", "agent client to wire up").choices(CLIENTS).default("claude-code")).option("--force", "overwrite existing workspace files instead of keeping them").allowExcessArguments(false).exitOverride((err) => {
1836
+ if (err.code === "commander.excessArguments") {
1837
+ process.stderr.write(excessArgumentsHint());
1838
+ }
1839
+ if (err.code === "commander.unknownOption" && err.message.includes("--host")) {
1840
+ process.stderr.write(`
1841
+ The flag that picks the agent client is ${cyan("--client")} (claude-code or cursor).
1842
+ ` + `--host is Publisher's own flag for the address its server binds to.
1843
+
1844
+ `);
1845
+ }
1846
+ process.exit(err.exitCode);
1847
+ }).action((name, options) => {
1848
+ run(name, options);
1849
+ });
1850
+ function startedAsProgram() {
1851
+ const entry = process.argv[1];
1852
+ if (entry === undefined) {
1853
+ return true;
1854
+ }
1855
+ try {
1856
+ return fs5.realpathSync(entry) === fs5.realpathSync(fileURLToPath2(import.meta.url));
1857
+ } catch {
1858
+ return true;
1859
+ }
1860
+ }
1861
+ if (startedAsProgram()) {
1862
+ program.parse();
1863
+ }
1864
+ export {
1865
+ snapshotArtifacts,
1866
+ formatSuccess,
1867
+ formatFailure,
1868
+ changesSince
1869
+ };