@loomweaver/cli 0.9.1 → 0.9.3

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.
Files changed (2) hide show
  1. package/dist/main.mjs +730 -189
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -98,34 +98,15 @@ function ensureBuildTarget(target, amendment, projectRoot) {
98
98
  added.push(...assets.added.map((entry) => `assets: ${entry}`));
99
99
  }
100
100
  next["options"] = options;
101
- if (amendment.inlineCritical !== void 0 || amendment.serviceWorker) {
102
- const configurations = { ...asObject(next["configurations"]) };
103
- const production = { ...asObject(configurations["production"]) };
104
- if (amendment.serviceWorker && production["serviceWorker"] === void 0) {
105
- production["serviceWorker"] = joinProjectPath(
106
- projectRoot,
107
- amendment.serviceWorker
108
- );
109
- added.push(`production serviceWorker: ${production["serviceWorker"]}`);
110
- }
111
- if (amendment.inlineCritical !== void 0) {
112
- const critical = ensureInlineCritical(
113
- production["optimization"],
114
- amendment.inlineCritical
115
- );
116
- if (critical.declined) {
117
- declined.push(
118
- "production optimization is a boolean, so inlineCritical cannot be set beside it \u2014 a release build then loads the stylesheet with an inline handler the generated content-security policy blocks, and renders unstyled"
119
- );
120
- } else if (critical.changed) {
121
- production["optimization"] = critical.value;
122
- added.push(
123
- `production optimization.styles.inlineCritical: ${amendment.inlineCritical}`
124
- );
125
- }
126
- }
127
- configurations["production"] = production;
128
- next["configurations"] = configurations;
101
+ if (amendment.inlineCritical !== void 0 || amendment.serviceWorker || amendment.initialBudget) {
102
+ const production = ensureProductionConfiguration(
103
+ next["configurations"],
104
+ amendment,
105
+ projectRoot
106
+ );
107
+ next["configurations"] = production.value;
108
+ added.push(...production.added);
109
+ declined.push(...production.declined);
129
110
  }
130
111
  return { value: next, added, declined };
131
112
  }
@@ -139,6 +120,99 @@ function ensureStylesheetSource(css, source) {
139
120
  @source '${source}';
140
121
  `;
141
122
  }
123
+ function ensureProductionConfiguration(value, amendment, projectRoot) {
124
+ const configurations = { ...asObject(value) };
125
+ const production = { ...asObject(configurations["production"]) };
126
+ const added = [];
127
+ const declined = [];
128
+ if (amendment.serviceWorker && production["serviceWorker"] === void 0) {
129
+ production["serviceWorker"] = joinProjectPath(
130
+ projectRoot,
131
+ amendment.serviceWorker
132
+ );
133
+ added.push(`production serviceWorker: ${production["serviceWorker"]}`);
134
+ }
135
+ if (amendment.inlineCritical !== void 0) {
136
+ const critical = ensureInlineCritical(
137
+ production["optimization"],
138
+ amendment.inlineCritical
139
+ );
140
+ if (critical.declined) {
141
+ declined.push(
142
+ "production optimization is a boolean, so inlineCritical cannot be set beside it \u2014 a release build then loads the stylesheet with an inline handler the generated content-security policy blocks, and renders unstyled"
143
+ );
144
+ } else if (critical.changed) {
145
+ production["optimization"] = critical.value;
146
+ added.push(
147
+ `production optimization.styles.inlineCritical: ${amendment.inlineCritical}`
148
+ );
149
+ }
150
+ }
151
+ if (amendment.initialBudget) {
152
+ const budgets = ensureInitialBudget(
153
+ production["budgets"],
154
+ amendment.initialBudget
155
+ );
156
+ if (budgets.changed) {
157
+ production["budgets"] = budgets.value;
158
+ added.push(
159
+ `production budget initial: ${amendment.initialBudget.warning} warning, ${amendment.initialBudget.error} error`
160
+ );
161
+ }
162
+ }
163
+ configurations["production"] = production;
164
+ return { value: configurations, added, declined };
165
+ }
166
+ function ensureInitialBudget(budgets, wanted) {
167
+ const entries = Array.isArray(budgets) ? [...budgets] : [];
168
+ const index = entries.findIndex(
169
+ (entry) => asObject(entry)?.["type"] === "initial"
170
+ );
171
+ if (index === -1) {
172
+ return {
173
+ value: [
174
+ ...entries,
175
+ {
176
+ type: "initial",
177
+ maximumWarning: wanted.warning,
178
+ maximumError: wanted.error
179
+ }
180
+ ],
181
+ changed: true
182
+ };
183
+ }
184
+ const current = asObject(entries[index]) ?? {};
185
+ if (!isBelow(current["maximumError"], wanted.error)) {
186
+ return { value: entries, changed: false };
187
+ }
188
+ entries[index] = {
189
+ ...current,
190
+ maximumWarning: wanted.warning,
191
+ maximumError: wanted.error
192
+ };
193
+ return { value: entries, changed: true };
194
+ }
195
+ function isBelow(recorded, wanted) {
196
+ const left = toBytes(recorded);
197
+ const right = toBytes(wanted);
198
+ return left !== void 0 && right !== void 0 && left < right;
199
+ }
200
+ function toBytes(size) {
201
+ if (typeof size !== "string") {
202
+ return void 0;
203
+ }
204
+ const match = /^([\d.]+)\s*(b|kb|mb|gb)?$/i.exec(size.trim());
205
+ if (!match) {
206
+ return void 0;
207
+ }
208
+ const units = {
209
+ b: 1,
210
+ kb: 1e3,
211
+ mb: 1e3 * 1e3,
212
+ gb: 1e3 * 1e3 * 1e3
213
+ };
214
+ return Number(match[1]) * units[(match[2] ?? "b").toLowerCase()];
215
+ }
142
216
  function ensureInlineCritical(optimization, inlineCritical) {
143
217
  if (typeof optimization === "boolean") {
144
218
  return { value: optimization, changed: false, declined: true };
@@ -276,10 +350,11 @@ function joinNamespaces(source, id) {
276
350
  if (names.includes(`'${id}'`)) {
277
351
  return { source, joined: true };
278
352
  }
353
+ const namespaces = [...names, `'${id}'`].join(", ");
279
354
  return {
280
355
  source: source.replace(
281
356
  NAMESPACES,
282
- () => `provideTranslationNamespaces(${[...names, `'${id}'`].join(", ")})`
357
+ () => `provideTranslationNamespaces(${namespaces})`
283
358
  ),
284
359
  joined: true
285
360
  };
@@ -346,15 +421,19 @@ function describeAmendment(amendment) {
346
421
  return `Add an @source entry for '${amendment.sourceRoot}' to the application's entry stylesheet, resolved from that stylesheet. Without it none of that code's utilities are emitted.`;
347
422
  }
348
423
  if (amendment.kind === "compose-plugin") {
349
- const providers = (amendment.providers ?? []).map((provider) => provider.line.replace(/,$/, ""));
350
- return `Register ${amendment.id} in the composition root: import { ${amendment.symbol} }, ${providers.map((line) => `${line}, `).join("")}provideTranslationNamespaces('${amendment.id}'), provideCapabilityGrants({ ${amendment.id}: [${amendment.capabilities.map((capability) => `'${capability}'`).join(
351
- ", "
352
- )}] }) and ...providePlugins(${amendment.symbol}). Without it none of its contributions appear.`;
424
+ const providers = (amendment.providers ?? []).map((provider) => `${provider.line.replace(/,$/, "")}, `).join("");
425
+ const capabilities = amendment.capabilities.map((capability) => `'${capability}'`).join(", ");
426
+ const imports = (amendment.providers ?? []).flatMap((provider) => provider.from ?? []).map(
427
+ (imported) => ` import { ${imported.symbols.join(", ")} } from '${imported.path}'.`
428
+ ).join("");
429
+ return `Register ${amendment.id} in the composition root: import { ${amendment.symbol} }, ${providers}provideTranslationNamespaces('${amendment.id}'), provideCapabilityGrants({ ${amendment.id}: [${capabilities}] }) and ...providePlugins(${amendment.symbol}).${imports} Without it none of its contributions appear.`;
353
430
  }
354
431
  return [
355
432
  ...amendment.styles.length > 0 ? [`name ${amendment.styles.join(", ")} in styles`] : [],
356
433
  ...amendment.assets.length > 0 ? [
357
- `add assets for ${amendment.assets.map((asset) => asset.input).join(
434
+ `add assets for ${amendment.assets.map(
435
+ (asset) => asset.output ? `${asset.input} served under ${asset.output}` : asset.input
436
+ ).join(
358
437
  ", "
359
438
  )} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
360
439
  ] : [],
@@ -363,7 +442,10 @@ function describeAmendment(amendment) {
363
442
  ] : [],
364
443
  ...amendment.inlineCritical === void 0 ? [] : [
365
444
  `set optimization.styles.inlineCritical to ${amendment.inlineCritical} in the production configuration (the generated content-security policy blocks the inline handler Angular's critical-CSS pass attaches, so a release build renders unstyled)`
366
- ]
445
+ ],
446
+ ...amendment.initialBudget ? [
447
+ `raise the initial bundle budget to ${amendment.initialBudget.warning} warning and ${amendment.initialBudget.error} error in the production configuration (a fresh workspace carries one sized for an empty application, and the workbench alone spends almost all of it, so a release build fails)`
448
+ ] : []
367
449
  ].map((step, index) => `${index + 1}. ${step}`).join(" ");
368
450
  }
369
451
 
@@ -755,7 +837,7 @@ function pause(): Promise<void> {
755
837
  }
756
838
 
757
839
  // ../devkit/src/recipes/angular-weaver/agent-files.ts
758
- var AG_UI_ADAPTER_VERSION = "0.9.1";
840
+ var AG_UI_ADAPTER_VERSION = "0.9.3";
759
841
  var AG_UI_PROTOCOL_VERSION = "0.0.x";
760
842
  function connectionFile(w) {
761
843
  return `import { signal } from '@angular/core';
@@ -1122,109 +1204,8 @@ function toneHelper() {
1122
1204
  ].join("\n");
1123
1205
  }
1124
1206
 
1125
- // ../devkit/src/recipes/angular-weaver/recipe.ts
1126
- var DEFAULT_MENU_SLOT = "content/tab/context";
1207
+ // ../devkit/src/recipes/angular-weaver/weaver-plugin.ts
1127
1208
  var SURFACE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>';
1128
- function accessLiteral(spec) {
1129
- if (spec === "authenticated") return "{ authenticated: true }";
1130
- if (spec === "anonymous") return "{ authenticated: false }";
1131
- if (spec.startsWith("role:")) {
1132
- const role = spec.slice("role:".length);
1133
- if (!role)
1134
- throw new Error('Access "role:" needs a role name, e.g. "role:admin".');
1135
- if (/['\\]/.test(role))
1136
- throw new Error(
1137
- `Access role "${role}" must not contain quotes or backslashes.`
1138
- );
1139
- return `{ anyRole: ['${role}'] }`;
1140
- }
1141
- throw new Error(
1142
- `Unknown access "${spec}". Use "authenticated", "anonymous" or "role:<name>".`
1143
- );
1144
- }
1145
- function resolveMenuSlot(menu) {
1146
- if (menu === true) {
1147
- return DEFAULT_MENU_SLOT;
1148
- }
1149
- return typeof menu === "string" && menu.length ? menu : void 0;
1150
- }
1151
- var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
1152
- "cmd",
1153
- "command",
1154
- "ctrl",
1155
- "control",
1156
- "meta"
1157
- ]);
1158
- function assertPlatformNeutralChord(shortcut) {
1159
- const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
1160
- const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
1161
- if (bound) {
1162
- throw new Error(
1163
- `Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
1164
- );
1165
- }
1166
- }
1167
- function resolveFeatures(id, input) {
1168
- const menuSlot = resolveMenuSlot(input?.menu);
1169
- const barItem = Boolean(input?.barItem);
1170
- const hasShortcut = Boolean(input?.shortcut?.trim());
1171
- if (hasShortcut) {
1172
- assertPlatformNeutralChord(input?.shortcut?.trim() ?? "");
1173
- }
1174
- const instanceable = Boolean(input?.instanceable);
1175
- const container = Boolean(input?.container);
1176
- if (instanceable && container) {
1177
- throw new Error(
1178
- 'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
1179
- );
1180
- }
1181
- const agent = Boolean(input?.agent);
1182
- return {
1183
- command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
1184
- menuSlot,
1185
- settings: Boolean(input?.settings),
1186
- access: input?.access ? accessLiteral(input.access) : void 0,
1187
- shortcut: input?.shortcut?.trim() || `mod+shift+${id.charAt(0)}`,
1188
- barItem,
1189
- about: Boolean(input?.about),
1190
- instanceable,
1191
- container,
1192
- agent,
1193
- spec: input?.spec !== false
1194
- };
1195
- }
1196
- function deriveCapabilities(features) {
1197
- const set = /* @__PURE__ */ new Set(["contributions", "navigation"]);
1198
- if (features.command) set.add("ui");
1199
- if (features.about) {
1200
- set.add("ui");
1201
- set.add("host");
1202
- }
1203
- if (features.agent) {
1204
- set.add("ui");
1205
- set.add("automation");
1206
- }
1207
- return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
1208
- }
1209
- function resolveWeaverInput(input) {
1210
- if (!isKebabId(input.id)) {
1211
- throw new Error(
1212
- `Weaver id must be kebab-case (e.g. "notes"); got "${input.id}".`
1213
- );
1214
- }
1215
- const features = resolveFeatures(input.id, input.features);
1216
- const capabilities = input.capabilities?.length ? [...input.capabilities] : deriveCapabilities(features);
1217
- return {
1218
- id: input.id,
1219
- name: input.name?.trim() || toTitleCase(input.id),
1220
- className: toPascalCase(input.id),
1221
- propertyName: toCamelCase(input.id),
1222
- capabilities,
1223
- features,
1224
- prefix: input.prefix?.trim() || "lw",
1225
- importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
1226
- };
1227
- }
1228
1209
  function containerChildIds(w) {
1229
1210
  return [`${w.id}.canvas`, `${w.id}.details`];
1230
1211
  }
@@ -1361,7 +1342,7 @@ function settingsBlock(w) {
1361
1342
  " });"
1362
1343
  ].join("\n");
1363
1344
  }
1364
- function pluginFile(w) {
1345
+ function pluginImports(w) {
1365
1346
  const imports = ["import { Plugin } from '@loomweaver/plugin-sdk';"];
1366
1347
  if (w.features.container) {
1367
1348
  imports.push(
@@ -1385,6 +1366,9 @@ function pluginFile(w) {
1385
1366
  if (w.features.settings) {
1386
1367
  imports.unshift("import { signal } from '@angular/core';");
1387
1368
  }
1369
+ return imports;
1370
+ }
1371
+ function pluginConsts(w) {
1388
1372
  const consts = [`const icon =
1389
1373
  '${SURFACE_ICON}';`];
1390
1374
  if (w.features.settings) {
@@ -1393,6 +1377,10 @@ function pluginFile(w) {
1393
1377
  `const ${w.propertyName}Note = signal('');`
1394
1378
  );
1395
1379
  }
1380
+ if (w.features.command) consts.push(toneHelper());
1381
+ return consts;
1382
+ }
1383
+ function pluginBody(w) {
1396
1384
  const body = [` ctx.contributeIcons({ '${w.id}': icon });`];
1397
1385
  if (w.features.agent) {
1398
1386
  body.push(
@@ -1407,14 +1395,17 @@ function pluginFile(w) {
1407
1395
  if (w.features.menuSlot) body.push(menuBlock(w));
1408
1396
  if (w.features.settings) body.push(settingsBlock(w));
1409
1397
  if (w.features.agent) body.push(agentSurfaceBlock(w));
1398
+ return body;
1399
+ }
1400
+ function pluginFile(w) {
1401
+ const body = pluginBody(w);
1410
1402
  const deactivate = w.features.agent ? `
1411
1403
  deactivate() {
1412
1404
  ${w.propertyName}Agent.set(null);
1413
1405
  },` : "";
1414
- if (w.features.command) consts.push(toneHelper());
1415
- return `${imports.join("\n")}
1406
+ return `${pluginImports(w).join("\n")}
1416
1407
 
1417
- ${consts.join("\n\n")}
1408
+ ${pluginConsts(w).join("\n\n")}
1418
1409
 
1419
1410
  export const ${w.propertyName}Plugin: Plugin = {
1420
1411
  manifest: {
@@ -1428,6 +1419,109 @@ ${body.join("\n")}
1428
1419
  };
1429
1420
  `;
1430
1421
  }
1422
+
1423
+ // ../devkit/src/recipes/angular-weaver/recipe.ts
1424
+ var DEFAULT_MENU_SLOT = "content/tab/context";
1425
+ function accessLiteral(spec) {
1426
+ if (spec === "authenticated") return "{ authenticated: true }";
1427
+ if (spec === "anonymous") return "{ authenticated: false }";
1428
+ if (spec.startsWith("role:")) {
1429
+ const role = spec.slice("role:".length);
1430
+ if (!role)
1431
+ throw new Error('Access "role:" needs a role name, e.g. "role:admin".');
1432
+ if (/['\\]/.test(role))
1433
+ throw new Error(
1434
+ `Access role "${role}" must not contain quotes or backslashes.`
1435
+ );
1436
+ return `{ anyRole: ['${role}'] }`;
1437
+ }
1438
+ throw new Error(
1439
+ `Unknown access "${spec}". Use "authenticated", "anonymous" or "role:<name>".`
1440
+ );
1441
+ }
1442
+ function resolveMenuSlot(menu) {
1443
+ if (menu === true) {
1444
+ return DEFAULT_MENU_SLOT;
1445
+ }
1446
+ return typeof menu === "string" && menu.length ? menu : void 0;
1447
+ }
1448
+ var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
1449
+ "cmd",
1450
+ "command",
1451
+ "ctrl",
1452
+ "control",
1453
+ "meta"
1454
+ ]);
1455
+ function assertPlatformNeutralChord(shortcut) {
1456
+ const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
1457
+ const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
1458
+ if (bound) {
1459
+ throw new Error(
1460
+ `Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
1461
+ );
1462
+ }
1463
+ }
1464
+ function resolveFeatures(id, input) {
1465
+ const menuSlot = resolveMenuSlot(input?.menu);
1466
+ const barItem = Boolean(input?.barItem);
1467
+ const hasShortcut = Boolean(input?.shortcut?.trim());
1468
+ if (hasShortcut) {
1469
+ assertPlatformNeutralChord(input?.shortcut?.trim() ?? "");
1470
+ }
1471
+ const instanceable = Boolean(input?.instanceable);
1472
+ const container = Boolean(input?.container);
1473
+ if (instanceable && container) {
1474
+ throw new Error(
1475
+ 'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
1476
+ );
1477
+ }
1478
+ const agent = Boolean(input?.agent);
1479
+ return {
1480
+ command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
1481
+ menuSlot,
1482
+ settings: Boolean(input?.settings),
1483
+ access: input?.access ? accessLiteral(input.access) : void 0,
1484
+ shortcut: input?.shortcut?.trim() || `mod+shift+${id.charAt(0)}`,
1485
+ barItem,
1486
+ about: Boolean(input?.about),
1487
+ instanceable,
1488
+ container,
1489
+ agent,
1490
+ spec: input?.spec !== false
1491
+ };
1492
+ }
1493
+ function deriveCapabilities(features) {
1494
+ const set = /* @__PURE__ */ new Set(["contributions", "navigation"]);
1495
+ if (features.command) set.add("ui");
1496
+ if (features.about) {
1497
+ set.add("ui");
1498
+ set.add("host");
1499
+ }
1500
+ if (features.agent) {
1501
+ set.add("ui");
1502
+ set.add("automation");
1503
+ }
1504
+ return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
1505
+ }
1506
+ function resolveWeaverInput(input) {
1507
+ if (!isKebabId(input.id)) {
1508
+ throw new Error(
1509
+ `Weaver id must be kebab-case (e.g. "notes"); got "${input.id}".`
1510
+ );
1511
+ }
1512
+ const features = resolveFeatures(input.id, input.features);
1513
+ const capabilities = input.capabilities?.length ? [...input.capabilities] : deriveCapabilities(features);
1514
+ return {
1515
+ id: input.id,
1516
+ name: input.name?.trim() || toTitleCase(input.id),
1517
+ className: toPascalCase(input.id),
1518
+ propertyName: toCamelCase(input.id),
1519
+ capabilities,
1520
+ features,
1521
+ prefix: input.prefix?.trim() || "lw",
1522
+ importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
1523
+ };
1524
+ }
1431
1525
  function aboutDialogFile(w) {
1432
1526
  return `import { Component, inject } from '@angular/core';
1433
1527
  import { DialogRef, PluginHost } from '@loomweaver/plugin-sdk';
@@ -1738,9 +1832,13 @@ var framePlugin = {
1738
1832
  };
1739
1833
 
1740
1834
  // ../devkit/src/recipes/platform-version.ts
1741
- var PLATFORM_VERSION = "0.9.1";
1835
+ var PLATFORM_VERSION = "0.9.3";
1742
1836
 
1743
1837
  // ../devkit/src/recipes/angular-distribution/amendments.ts
1838
+ var DISTRIBUTION_INITIAL_BUDGET = {
1839
+ warning: "1.5MB",
1840
+ error: "2MB"
1841
+ };
1744
1842
  function distributionAmendments(d) {
1745
1843
  return [
1746
1844
  {
@@ -1774,7 +1872,8 @@ function distributionAmendments(d) {
1774
1872
  }
1775
1873
  ],
1776
1874
  serviceWorker: "ngsw-config.json",
1777
- inlineCritical: false
1875
+ inlineCritical: false,
1876
+ initialBudget: DISTRIBUTION_INITIAL_BUDGET
1778
1877
  }
1779
1878
  ];
1780
1879
  }
@@ -1969,9 +2068,10 @@ function readme2(d) {
1969
2068
  "the stylesheet with an **inline** `onload` handler that the policy blocks \u2014 the app then renders",
1970
2069
  "completely unstyled, and only in production builds.",
1971
2070
  "",
1972
- "One thing is still yours, because the scaffold cannot know your budget: a production build warns",
1973
- "that the initial bundle exceeds Angular's 500 kB default. The shell is a whole application",
1974
- "chrome, so raise the budgets in your build target.",
2071
+ "The initial-bundle budget in your build target was raised for the same reason: the shell is a",
2072
+ "whole application chrome and spends most of Angular's default before your own code arrives, so",
2073
+ "a production build would have failed on a number meant for an empty app. It is still a budget,",
2074
+ "and it is yours to move.",
1975
2075
  "",
1976
2076
  "## Ship less than the whole workbench",
1977
2077
  "",
@@ -2024,7 +2124,7 @@ function resolveDistributionInput(input) {
2024
2124
  );
2025
2125
  }
2026
2126
  const directory = input.directory === void 0 ? `apps/${input.name}` : input.directory.trim();
2027
- const depth = directory.split("/").filter(Boolean).length;
2127
+ const depth = normalizeProjectRoot(directory).split("/").filter(Boolean).length;
2028
2128
  return {
2029
2129
  name: input.name,
2030
2130
  title: input.title?.trim() || toTitleCase(input.name),
@@ -2744,10 +2844,10 @@ function weaverAmendments(input, where) {
2744
2844
  version: AG_UI_PROTOCOL_VERSION
2745
2845
  }
2746
2846
  ] : [];
2747
- const directory = normalizeProjectRoot(where ?? "");
2748
- if (!directory) {
2847
+ if (where === void 0 || where === "") {
2749
2848
  return packages;
2750
2849
  }
2850
+ const directory = normalizeProjectRoot(where);
2751
2851
  return [
2752
2852
  ...packages,
2753
2853
  {
@@ -2756,19 +2856,19 @@ function weaverAmendments(input, where) {
2756
2856
  assets: [
2757
2857
  {
2758
2858
  glob: "**/*.json",
2759
- input: `${directory}/src/lib/i18n`,
2859
+ input: joinProjectPath(directory, "src/lib/i18n"),
2760
2860
  from: "workspace",
2761
2861
  output: `i18n/${w.id}`
2762
2862
  }
2763
2863
  ]
2764
2864
  },
2765
- { kind: "stylesheet-source", sourceRoot: `${directory}/src` },
2865
+ { kind: "stylesheet-source", sourceRoot: joinProjectPath(directory, "src") },
2766
2866
  {
2767
2867
  kind: "compose-plugin",
2768
2868
  id: w.id,
2769
2869
  symbol: `${w.propertyName}Plugin`,
2770
2870
  capabilities: w.capabilities,
2771
- sourceRoot: `${directory}/src`
2871
+ sourceRoot: joinProjectPath(directory, "src")
2772
2872
  }
2773
2873
  ];
2774
2874
  }
@@ -2776,10 +2876,10 @@ function weaverAmendments(input, where) {
2776
2876
  // ../devkit/src/recipes/auth-source/recipe-amendments.ts
2777
2877
  function authSourceAmendments(input, where) {
2778
2878
  const a = resolveAuthSourceInput(input);
2779
- const directory = normalizeProjectRoot(where ?? "");
2780
- if (a.bare || !directory) {
2879
+ if (a.bare || where === void 0 || where === "") {
2781
2880
  return [];
2782
2881
  }
2882
+ const directory = normalizeProjectRoot(where);
2783
2883
  return [
2784
2884
  {
2785
2885
  kind: "build-target",
@@ -2787,7 +2887,7 @@ function authSourceAmendments(input, where) {
2787
2887
  assets: [
2788
2888
  {
2789
2889
  glob: "**/*.json",
2790
- input: `${directory}/i18n`,
2890
+ input: joinProjectPath(directory, "i18n"),
2791
2891
  from: "workspace",
2792
2892
  output: `i18n/${SESSION_PLUGIN_ID}`
2793
2893
  }
@@ -3488,7 +3588,7 @@ function read(ts, file, call) {
3488
3588
  return {
3489
3589
  at: at2,
3490
3590
  id: id.text,
3491
- callable: callable !== void 0 && callable.kind === ts.SyntaxKind.TrueKeyword,
3591
+ callable: callable?.kind === ts.SyntaxKind.TrueKeyword,
3492
3592
  described: properties.has("description"),
3493
3593
  answers: properties.has("answers"),
3494
3594
  returnsValue: returnsValue(ts, properties.get("run")),
@@ -3616,8 +3716,9 @@ function loadTypeScript(fromDir) {
3616
3716
  }
3617
3717
 
3618
3718
  // src/lib/run.ts
3619
- import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
3620
- import { join } from "node:path";
3719
+ import { spawnSync } from "node:child_process";
3720
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
3721
+ import { join as join4 } from "node:path";
3621
3722
 
3622
3723
  // src/lib/args.ts
3623
3724
  var ArgError = class extends Error {
@@ -3963,10 +4064,7 @@ var Amender = class {
3963
4064
  if (!/@import\s+['"]tailwindcss['"]/.test(css)) {
3964
4065
  return;
3965
4066
  }
3966
- const source = posix.relative(
3967
- this.displayName(dirname2(entry)),
3968
- amendment.sourceRoot
3969
- );
4067
+ const source = posix.relative(this.displayName(dirname2(entry)), amendment.sourceRoot) || ".";
3970
4068
  const next = ensureStylesheetSource(css, source);
3971
4069
  if (next === css) {
3972
4070
  return;
@@ -4093,8 +4191,409 @@ function relativeImport(fromDir, sourceRoot) {
4093
4191
  return path.startsWith(".") ? path : `./${path}`;
4094
4192
  }
4095
4193
 
4194
+ // src/lib/init.ts
4195
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
4196
+ import { join as join3 } from "node:path";
4197
+
4198
+ // src/lib/init-plan.ts
4199
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
4200
+ import { basename as basename2, join as join2, resolve as resolve5 } from "node:path";
4201
+
4202
+ // src/lib/nx-applications.ts
4203
+ import { existsSync as existsSync3, readdirSync } from "node:fs";
4204
+ import { basename, join, relative as relative3, sep as sep3 } from "node:path";
4205
+ var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "tmp", "coverage"]);
4206
+ function nxApplications(workspaceRoot) {
4207
+ const found = [];
4208
+ const walk = (folder) => {
4209
+ const project = join(folder, "project.json");
4210
+ if (existsSync3(project)) {
4211
+ const config = readJsonFile(project);
4212
+ const name = typeof config["name"] === "string" ? config["name"] : basename(folder);
4213
+ if (config["projectType"] === "application" && !name.endsWith("-e2e")) {
4214
+ found.push({
4215
+ name,
4216
+ root: relative3(workspaceRoot, folder).split(sep3).join("/")
4217
+ });
4218
+ }
4219
+ }
4220
+ for (const entry of readdirSync(folder, { withFileTypes: true })) {
4221
+ if (!entry.isDirectory() || SKIPPED.has(entry.name) || entry.name.startsWith(".")) {
4222
+ continue;
4223
+ }
4224
+ walk(join(folder, entry.name));
4225
+ }
4226
+ };
4227
+ walk(workspaceRoot);
4228
+ return found.toSorted((a, b) => a.name.localeCompare(b.name));
4229
+ }
4230
+
4231
+ // src/lib/package-manager.ts
4232
+ import { existsSync as existsSync4 } from "node:fs";
4233
+ import { resolve as resolve4 } from "node:path";
4234
+ var PACKAGE_MANAGERS = [
4235
+ "npm",
4236
+ "pnpm",
4237
+ "yarn",
4238
+ "bun"
4239
+ ];
4240
+ var LOCKFILES = [
4241
+ ["package-lock.json", "npm"],
4242
+ ["npm-shrinkwrap.json", "npm"],
4243
+ ["pnpm-lock.yaml", "pnpm"],
4244
+ ["yarn.lock", "yarn"],
4245
+ ["bun.lock", "bun"],
4246
+ ["bun.lockb", "bun"]
4247
+ ];
4248
+ function detectPackageManager(root) {
4249
+ for (const [lockfile, manager] of LOCKFILES) {
4250
+ if (existsSync4(resolve4(root, lockfile))) {
4251
+ return { manager, lockfile };
4252
+ }
4253
+ }
4254
+ return { manager: "npm" };
4255
+ }
4256
+ function packageManagerFrom(value) {
4257
+ if (value === void 0) {
4258
+ return void 0;
4259
+ }
4260
+ if (!PACKAGE_MANAGERS.includes(value)) {
4261
+ throw new ArgError(
4262
+ `Option --package-manager must be one of: ${PACKAGE_MANAGERS.join(", ")}.`
4263
+ );
4264
+ }
4265
+ return value;
4266
+ }
4267
+ function installCommand(manager, packages, dev) {
4268
+ const devFlag = manager === "bun" ? "-d" : "-D";
4269
+ const verb = manager === "npm" ? "install" : "add";
4270
+ return [manager, verb, ...dev ? [devFlag] : [], ...packages];
4271
+ }
4272
+ function execCommand(manager, argv) {
4273
+ if (manager === "npm") {
4274
+ return ["npx", ...argv];
4275
+ }
4276
+ if (manager === "pnpm") {
4277
+ return ["pnpm", "exec", ...argv];
4278
+ }
4279
+ if (manager === "bun") {
4280
+ return ["bunx", ...argv];
4281
+ }
4282
+ return ["yarn", ...argv];
4283
+ }
4284
+ function runScriptCommand(manager, script) {
4285
+ return `${manager} ${script}`;
4286
+ }
4287
+
4288
+ // src/lib/init-plan.ts
4289
+ var RUNTIME_PACKAGES = [
4290
+ "@loomweaver/shell",
4291
+ "@loomweaver/plugin-sdk",
4292
+ "@loomweaver/frame-kit",
4293
+ "@angular/cdk",
4294
+ "@jsverse/transloco",
4295
+ "@ng-icons/heroicons"
4296
+ ];
4297
+ var STYLE_PACKAGES = [
4298
+ "tailwindcss",
4299
+ "@tailwindcss/postcss",
4300
+ "@tailwindcss/typography"
4301
+ ];
4302
+ var SERVICE_WORKER = "@angular/service-worker";
4303
+ var NX_COLLECTION = "@loomweaver/devkit";
4304
+ function planInit(args, deps) {
4305
+ const found = findWorkspace(deps.cwd);
4306
+ if (!found?.kind) {
4307
+ throw new WorkspaceError(
4308
+ `No Angular application or Nx workspace here: expected an angular.json or an nx.json at or above ${resolve5(deps.cwd)}. Create one first ("ng new my-studio --style=css --ssr=false", or "npx create-nx-workspace") and run init inside it.`
4309
+ );
4310
+ }
4311
+ const workspace = found;
4312
+ const manifest2 = readManifest(workspace.root);
4313
+ const override = packageManagerFrom(stringFlag(args, "package-manager"));
4314
+ const detected = detectPackageManager(workspace.root);
4315
+ const app = workspace.kind === "nx" ? chooseApplication(workspace.root, stringFlag(args, "app")) : void 0;
4316
+ const name = app?.name ?? kebab(manifest2.name ?? basename2(workspace.root));
4317
+ const styles = stringFlag(args, "styles") ?? "tailwind";
4318
+ const declared = { ...manifest2.dependencies, ...manifest2.devDependencies };
4319
+ const missing = (names) => names.filter((candidate) => !Object.hasOwn(declared, candidate));
4320
+ return {
4321
+ workspace,
4322
+ manager: override ?? detected.manager,
4323
+ lockfile: override ? void 0 : detected.lockfile,
4324
+ name,
4325
+ title: stringFlag(args, "title") ?? titleCase(name),
4326
+ styles,
4327
+ weaver: weaverId(args),
4328
+ runtime: [
4329
+ ...missing(RUNTIME_PACKAGES),
4330
+ ...Object.hasOwn(declared, SERVICE_WORKER) ? [] : [serviceWorkerSpec(workspace.root, manifest2)]
4331
+ ],
4332
+ dev: [
4333
+ ...styles === "tailwind" ? missing(STYLE_PACKAGES) : [],
4334
+ ...workspace.kind === "nx" && !Object.hasOwn(declared, NX_COLLECTION) ? [collectionSpec(deps.version)] : []
4335
+ ],
4336
+ app,
4337
+ dryRun: boolFlag(args, "dry-run") === true
4338
+ };
4339
+ }
4340
+ function chooseApplication(root, wanted) {
4341
+ const apps = nxApplications(root);
4342
+ const names = apps.map((candidate) => candidate.name).join(", ");
4343
+ if (wanted) {
4344
+ const app = apps.find((candidate) => candidate.name === wanted);
4345
+ if (!app) {
4346
+ throw new WorkspaceError(
4347
+ `No application named "${wanted}" in this workspace${apps.length > 0 ? `; there are: ${names}` : ""}.`
4348
+ );
4349
+ }
4350
+ return app;
4351
+ }
4352
+ if (apps.length === 1) {
4353
+ return apps[0];
4354
+ }
4355
+ if (apps.length === 0) {
4356
+ throw new WorkspaceError(
4357
+ 'This Nx workspace has no application (no project.json with "projectType": "application"). Generate one first, then run init with --app <name>.'
4358
+ );
4359
+ }
4360
+ throw new WorkspaceError(
4361
+ `This Nx workspace has ${apps.length} applications, so none was chosen: ${names}. Name one with --app.`
4362
+ );
4363
+ }
4364
+ function weaverId(args) {
4365
+ const value = args.flags["weaver"];
4366
+ if (value === false) {
4367
+ return void 0;
4368
+ }
4369
+ if (value === true) {
4370
+ throw new ArgError(
4371
+ "Option --weaver needs a plugin id, or pass --no-weaver."
4372
+ );
4373
+ }
4374
+ return value ?? "notes";
4375
+ }
4376
+ function readManifest(root) {
4377
+ const file = join2(root, "package.json");
4378
+ return existsSync5(file) ? readJsonFile(file) : {};
4379
+ }
4380
+ function serviceWorkerSpec(root, manifest2) {
4381
+ const installed = join2(root, "node_modules/@angular/core/package.json");
4382
+ if (existsSync5(installed)) {
4383
+ const { version } = JSON.parse(readFileSync3(installed, "utf8"));
4384
+ return `${SERVICE_WORKER}@${version}`;
4385
+ }
4386
+ const declared = manifest2.dependencies?.["@angular/core"];
4387
+ return declared ? `${SERVICE_WORKER}@${declared}` : SERVICE_WORKER;
4388
+ }
4389
+ function collectionSpec(version) {
4390
+ return version === "0.0.0" ? `${NX_COLLECTION}@latest` : `${NX_COLLECTION}@${version}`;
4391
+ }
4392
+ function kebab(value) {
4393
+ const unscoped = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
4394
+ return unscoped.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
4395
+ }
4396
+ function titleCase(name) {
4397
+ return name.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4398
+ }
4399
+
4400
+ // src/lib/init.ts
4401
+ var INIT_FLAGS = [
4402
+ "title",
4403
+ "styles",
4404
+ "weaver",
4405
+ "app",
4406
+ "package-manager",
4407
+ "dry-run"
4408
+ ];
4409
+ function init(args, io, deps) {
4410
+ rejectUnknownFlags(args, INIT_FLAGS);
4411
+ const plan = planInit(args, deps);
4412
+ report(io, plan);
4413
+ let didSomething = false;
4414
+ for (const step of stepsFor(plan)) {
4415
+ if (step.done) {
4416
+ io.out(`${step.label}: already there.`);
4417
+ continue;
4418
+ }
4419
+ didSomething = true;
4420
+ const code = perform(step, plan, io, deps);
4421
+ if (code !== 0) {
4422
+ io.err(`${step.label} failed; nothing after it was run.`);
4423
+ return code;
4424
+ }
4425
+ }
4426
+ if (!plan.weaver) {
4427
+ io.out(
4428
+ "First weaver: skipped as asked; the rail stays empty until a plugin is composed in."
4429
+ );
4430
+ }
4431
+ if (!didSomething) {
4432
+ io.out(
4433
+ "Nothing left to do: the packages, the distribution and the weaver are all in place."
4434
+ );
4435
+ }
4436
+ io.out(
4437
+ `${plan.dryRun ? "Then serve" : "Serve"} it with: ${serveCommand(plan)}`
4438
+ );
4439
+ return 0;
4440
+ }
4441
+ function perform(step, plan, io, deps) {
4442
+ if (step.command) {
4443
+ if (plan.dryRun) {
4444
+ io.out(`Would run: ${step.command.join(" ")}`);
4445
+ return 0;
4446
+ }
4447
+ io.out(`${step.label}: ${step.describe}`);
4448
+ deps.exec(step.command, plan.workspace.root);
4449
+ return 0;
4450
+ }
4451
+ if (step.argv) {
4452
+ io.out(
4453
+ plan.dryRun ? `Would ${step.label.toLowerCase()}: ${step.describe}` : `${step.label}: ${step.describe}`
4454
+ );
4455
+ if (plan.dryRun && step.after) {
4456
+ io.out(` (${step.after})`);
4457
+ }
4458
+ return deps.run(plan.dryRun ? [...step.argv, "--dry-run"] : step.argv);
4459
+ }
4460
+ return 0;
4461
+ }
4462
+ function report(io, plan) {
4463
+ const where = plan.workspace.kind === "nx" ? `Nx workspace, application ${plan.app?.name}` : "Angular CLI application";
4464
+ io.out(
4465
+ `${plan.dryRun ? "Would take" : "Taking"} ${plan.workspace.root} to a product: ${where}, packages with ${managerLabel(plan)}.`
4466
+ );
4467
+ }
4468
+ function managerLabel(plan) {
4469
+ if (plan.lockfile) {
4470
+ return `${plan.manager} (from ${plan.lockfile})`;
4471
+ }
4472
+ if (plan.manager === "npm") {
4473
+ return "npm (no lockfile found, so npm)";
4474
+ }
4475
+ return plan.manager;
4476
+ }
4477
+ function stepsFor(plan) {
4478
+ return plan.workspace.kind === "nx" ? nxSteps(plan) : angularSteps(plan);
4479
+ }
4480
+ function angularSteps(plan) {
4481
+ const root = plan.workspace.root;
4482
+ const steps = [
4483
+ ...installSteps(plan),
4484
+ {
4485
+ label: "Distribution",
4486
+ describe: `scaffold the composition root for "${plan.title}" (${plan.styles}) and wire the build`,
4487
+ done: composesShell(join3(root, "src/app/app.config.ts")),
4488
+ argv: [
4489
+ "distribution",
4490
+ "--name",
4491
+ plan.name,
4492
+ "--title",
4493
+ plan.title,
4494
+ "--styles",
4495
+ plan.styles,
4496
+ "--out",
4497
+ root,
4498
+ "--force"
4499
+ ]
4500
+ }
4501
+ ];
4502
+ if (plan.weaver) {
4503
+ steps.push({
4504
+ label: "First weaver",
4505
+ describe: weaverDescription(plan.weaver),
4506
+ done: existsSync6(join3(root, "src", plan.weaver, "src/index.ts")),
4507
+ after: "planned against the composition root as it is now; once the distribution step has run, the weaver composes into it",
4508
+ argv: [
4509
+ "weaver",
4510
+ "--id",
4511
+ plan.weaver,
4512
+ "--command",
4513
+ "--out",
4514
+ join3(root, "src", plan.weaver)
4515
+ ]
4516
+ });
4517
+ }
4518
+ return steps;
4519
+ }
4520
+ function nxSteps(plan) {
4521
+ const root = plan.workspace.root;
4522
+ const app = plan.app;
4523
+ if (!app) {
4524
+ return [];
4525
+ }
4526
+ const generator = (name, argv) => execCommand(plan.manager, ["nx", "g", `${NX_COLLECTION}:${name}`, ...argv]);
4527
+ const steps = [
4528
+ ...installSteps(plan),
4529
+ {
4530
+ label: "Distribution",
4531
+ describe: `nx g ${NX_COLLECTION}:distribution over ${app.name} for "${plan.title}" (${plan.styles})`,
4532
+ done: composesShell(join3(root, app.root, "src/app/app.config.ts")),
4533
+ command: generator("distribution", [
4534
+ "--name",
4535
+ app.name,
4536
+ "--directory",
4537
+ app.root,
4538
+ "--title",
4539
+ plan.title,
4540
+ "--styles",
4541
+ plan.styles,
4542
+ "--force"
4543
+ ])
4544
+ }
4545
+ ];
4546
+ if (plan.weaver) {
4547
+ steps.push({
4548
+ label: "First weaver",
4549
+ describe: weaverDescription(plan.weaver),
4550
+ done: existsSync6(join3(root, "libs", `${plan.weaver}-weaver`)),
4551
+ command: generator("weaver", [
4552
+ "--id",
4553
+ plan.weaver,
4554
+ "--command",
4555
+ "--app",
4556
+ app.name
4557
+ ])
4558
+ });
4559
+ }
4560
+ return steps;
4561
+ }
4562
+ function installSteps(plan) {
4563
+ return [
4564
+ {
4565
+ label: "Install",
4566
+ describe: plan.runtime.join(" "),
4567
+ done: plan.runtime.length === 0,
4568
+ command: installCommand(plan.manager, plan.runtime, false)
4569
+ },
4570
+ {
4571
+ label: "Install for development",
4572
+ describe: plan.dev.join(" "),
4573
+ done: plan.dev.length === 0,
4574
+ command: installCommand(plan.manager, plan.dev, true)
4575
+ }
4576
+ ];
4577
+ }
4578
+ function weaverDescription(id) {
4579
+ return `"${id}" with a command on mod+shift+${id.charAt(0)}`;
4580
+ }
4581
+ function serveCommand(plan) {
4582
+ if (plan.workspace.kind === "nx") {
4583
+ return execCommand(plan.manager, [
4584
+ "nx",
4585
+ "serve",
4586
+ plan.app?.name ?? ""
4587
+ ]).join(" ");
4588
+ }
4589
+ return runScriptCommand(plan.manager, "start");
4590
+ }
4591
+ function composesShell(file) {
4592
+ return existsSync6(file) && readFileSync4(file, "utf8").includes("provideShell(");
4593
+ }
4594
+
4096
4595
  // src/lib/scaffold.ts
4097
- import { relative as relative3, resolve as resolve4 } from "node:path";
4596
+ import { relative as relative4, resolve as resolve6 } from "node:path";
4098
4597
  function findScaffold2(name) {
4099
4598
  const scaffold2 = findScaffold(name);
4100
4599
  if (!scaffold2) {
@@ -4111,16 +4610,16 @@ function allowedFlagsFor(scaffold2) {
4111
4610
  ]);
4112
4611
  }
4113
4612
  function readFlag(args, option) {
4114
- const kebab = kebabCase(option.name);
4115
- const value = args.flags[kebab] ?? args.flags[option.name];
4613
+ const kebab2 = kebabCase(option.name);
4614
+ const value = args.flags[kebab2] ?? args.flags[option.name];
4116
4615
  if (value === void 0) {
4117
4616
  return void 0;
4118
4617
  }
4119
4618
  if (option.type === "string" && typeof value !== "string") {
4120
- throw new ArgError(`Option --${kebab} needs a value.`);
4619
+ throw new ArgError(`Option --${kebab2} needs a value.`);
4121
4620
  }
4122
4621
  if (option.type === "boolean" && typeof value !== "boolean") {
4123
- throw new ArgError(`Option --${kebab} does not take a value.`);
4622
+ throw new ArgError(`Option --${kebab2} does not take a value.`);
4124
4623
  }
4125
4624
  return value;
4126
4625
  }
@@ -4130,9 +4629,7 @@ function valuesFor(scaffold2, args) {
4130
4629
  const value = readFlag(args, option);
4131
4630
  if (value === void 0) {
4132
4631
  if (option.required) {
4133
- throw new ArgError(
4134
- `Option --${kebabCase(option.name)} is required.`
4135
- );
4632
+ throw new ArgError(`Option --${kebabCase(option.name)} is required.`);
4136
4633
  }
4137
4634
  continue;
4138
4635
  }
@@ -4146,8 +4643,13 @@ function valuesFor(scaffold2, args) {
4146
4643
  return values;
4147
4644
  }
4148
4645
  function directoryFromOut(out) {
4149
- const below = relative3(process.cwd(), resolve4(out ?? "."));
4150
- return below.startsWith("..") ? "" : below;
4646
+ const target = resolve6(out ?? ".");
4647
+ const base = findWorkspace(target)?.root ?? process.cwd();
4648
+ const below = relative4(base, target);
4649
+ if (below.startsWith("..")) {
4650
+ return "";
4651
+ }
4652
+ return below || ".";
4151
4653
  }
4152
4654
  function scaffoldValues(scaffold2, args) {
4153
4655
  const values = valuesFor(scaffold2, args);
@@ -4178,16 +4680,16 @@ import {
4178
4680
  rmSync,
4179
4681
  writeFileSync as writeFileSync2
4180
4682
  } from "node:fs";
4181
- import { dirname as dirname3, isAbsolute, relative as relative4, resolve as resolve5 } from "node:path";
4683
+ import { dirname as dirname3, isAbsolute, relative as relative5, resolve as resolve7 } from "node:path";
4182
4684
  var WriteError = class extends Error {
4183
4685
  };
4184
4686
  function planWrite(files, root) {
4185
- const absoluteRoot = resolve5(root);
4687
+ const absoluteRoot = resolve7(root);
4186
4688
  const planned = [];
4187
4689
  const conflicts = [];
4188
4690
  for (const path of Object.keys(files).toSorted((a, b) => a.localeCompare(b))) {
4189
- const absolute = resolve5(absoluteRoot, path);
4190
- const inside = relative4(absoluteRoot, absolute);
4691
+ const absolute = resolve7(absoluteRoot, path);
4692
+ const inside = relative5(absoluteRoot, absolute);
4191
4693
  if (inside.startsWith("..") || isAbsolute(inside)) {
4192
4694
  throw new WriteError(`Refusing to write outside the target directory: ${path}`);
4193
4695
  }
@@ -4215,7 +4717,7 @@ function entryExists(absolute) {
4215
4717
  }
4216
4718
  }
4217
4719
  function assertResolvesInsideRoot(root, path, absolute) {
4218
- const inside = relative4(realpathSync(root), realpathSync(dirname3(absolute)));
4720
+ const inside = relative5(realpathSync(root), realpathSync(dirname3(absolute)));
4219
4721
  if (inside.startsWith("..") || isAbsolute(inside)) {
4220
4722
  throw new WriteError(
4221
4723
  `Refusing to write through a link that leaves the target directory: ${path}`
@@ -4229,7 +4731,7 @@ function replaceSymlinkEntry(absolute) {
4229
4731
  }
4230
4732
 
4231
4733
  // src/lib/run.ts
4232
- var VERSION = "0.9.1";
4734
+ var VERSION = "0.9.3";
4233
4735
  function help() {
4234
4736
  const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
4235
4737
  return [
@@ -4241,6 +4743,10 @@ function help() {
4241
4743
  ...commands,
4242
4744
  "",
4243
4745
  "Other commands:",
4746
+ " init take the Angular application or Nx workspace you are in to a running product:",
4747
+ " install the platform, scaffold the distribution and a first weaver, say what to serve",
4748
+ " [--title <t>] [--styles tailwind|precompiled] [--weaver <id>|--no-weaver] [--app <nx app>]",
4749
+ " [--package-manager npm|pnpm|yarn|bun] [--dry-run]",
4244
4750
  " list print every scaffold with its options",
4245
4751
  " validate-manifest --id <id> [--name <name>] [--capabilities <a,b>]",
4246
4752
  " validate-i18n --dir <dir> check <lang>.json bundles for key parity",
@@ -4299,15 +4805,17 @@ function validateManifestCommand(args, io) {
4299
4805
  }
4300
4806
  function readBundles(dir) {
4301
4807
  const bundles = {};
4302
- for (const entry of readdirSync(dir)) {
4808
+ for (const entry of readdirSync2(dir)) {
4303
4809
  if (!entry.endsWith(".json")) {
4304
4810
  continue;
4305
4811
  }
4306
4812
  const language = entry.slice(0, -".json".length);
4307
4813
  try {
4308
- bundles[language] = JSON.parse(readFileSync3(join(dir, entry), "utf8"));
4814
+ bundles[language] = JSON.parse(readFileSync5(join4(dir, entry), "utf8"));
4309
4815
  } catch (error) {
4310
- throw new ArgError(`${entry} is not valid JSON: ${error.message}`);
4816
+ throw new ArgError(
4817
+ `${entry} is not valid JSON: ${error.message}`
4818
+ );
4311
4819
  }
4312
4820
  }
4313
4821
  if (Object.keys(bundles).length === 0) {
@@ -4326,15 +4834,15 @@ function validateI18nCommand(args, io) {
4326
4834
  function readSources(dir) {
4327
4835
  const sources = [];
4328
4836
  const walk = (folder) => {
4329
- for (const entry of readdirSync(folder, { withFileTypes: true })) {
4837
+ for (const entry of readdirSync2(folder, { withFileTypes: true })) {
4330
4838
  if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
4331
4839
  continue;
4332
4840
  }
4333
- const path = join(folder, entry.name);
4841
+ const path = join4(folder, entry.name);
4334
4842
  if (entry.isDirectory()) {
4335
4843
  walk(path);
4336
4844
  } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".spec.ts")) {
4337
- sources.push({ path, text: readFileSync3(path, "utf8") });
4845
+ sources.push({ path, text: readFileSync5(path, "utf8") });
4338
4846
  }
4339
4847
  }
4340
4848
  };
@@ -4357,19 +4865,25 @@ function validateCommandsCommand(args, io) {
4357
4865
  `typescript is not installed where ${dir} can reach it; the check reads sources with the TypeScript compiler API, so run it inside the project.`
4358
4866
  );
4359
4867
  }
4360
- return reportFindings(io, validateCommands(readSources(dir), ts), boolFlag(args, "strict") === true);
4868
+ return reportFindings(
4869
+ io,
4870
+ validateCommands(readSources(dir), ts),
4871
+ boolFlag(args, "strict") === true
4872
+ );
4361
4873
  }
4362
4874
  function readCatalog(file) {
4363
4875
  let raw;
4364
4876
  try {
4365
- raw = readFileSync3(file, "utf8");
4877
+ raw = readFileSync5(file, "utf8");
4366
4878
  } catch (error) {
4367
4879
  throw new ArgError(`Cannot read ${file}: ${error.message}`);
4368
4880
  }
4369
4881
  try {
4370
4882
  return JSON.parse(raw);
4371
4883
  } catch (error) {
4372
- throw new ArgError(`${file} is not valid JSON: ${error.message}`);
4884
+ throw new ArgError(
4885
+ `${file} is not valid JSON: ${error.message}`
4886
+ );
4373
4887
  }
4374
4888
  }
4375
4889
  function validateCatalogCommand(args, io) {
@@ -4446,7 +4960,26 @@ function scaffold(args, io) {
4446
4960
  reportAmendments(io, amend, true);
4447
4961
  return 0;
4448
4962
  }
4449
- function run(argv, io) {
4963
+ var ExecError = class extends Error {
4964
+ };
4965
+ function execInherit(command, cwd) {
4966
+ const result = spawnSync(command[0], command.slice(1), {
4967
+ cwd,
4968
+ stdio: "inherit",
4969
+ shell: process.platform === "win32"
4970
+ });
4971
+ if (result.error) {
4972
+ throw new ExecError(
4973
+ `Could not run "${command.join(" ")}": ${result.error.message}`
4974
+ );
4975
+ }
4976
+ if (result.status !== 0) {
4977
+ throw new ExecError(
4978
+ `"${command.join(" ")}" exited with ${result.status ?? "a signal"}.`
4979
+ );
4980
+ }
4981
+ }
4982
+ function run(argv, io, deps) {
4450
4983
  let args;
4451
4984
  try {
4452
4985
  args = parseArgs(argv);
@@ -4467,6 +5000,14 @@ function run(argv, io) {
4467
5000
  return 1;
4468
5001
  }
4469
5002
  try {
5003
+ if (args.command === "init") {
5004
+ return init(args, io, {
5005
+ cwd: deps?.cwd ?? process.cwd(),
5006
+ exec: deps?.exec ?? execInherit,
5007
+ run: deps?.run ?? ((inner) => run(inner, io, deps)),
5008
+ version: deps?.version ?? VERSION
5009
+ });
5010
+ }
4470
5011
  if (args.command === "list") {
4471
5012
  return list(args, io);
4472
5013
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomweaver/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "LoomWeaver: open-source plugin platform for Angular workbenches. The scaffolding CLI: generates weavers, distributions and integrations into any project, with no Nx workspace and no checkout required.",
5
5
  "keywords": [
6
6
  "loomweaver",