@xenonbyte/da-vinci-workflow 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -29,6 +29,19 @@ const {
29
29
  endPencilSession,
30
30
  getPencilSessionStatus
31
31
  } = require("./pencil-session");
32
+ const {
33
+ searchIconLibrary,
34
+ formatIconSearchReport
35
+ } = require("./icon-search");
36
+ const {
37
+ syncIconCatalog,
38
+ loadIconCatalog,
39
+ formatIconSyncReport
40
+ } = require("./icon-sync");
41
+ const {
42
+ loadIconAliases,
43
+ expandQueryWithAliases
44
+ } = require("./icon-aliases");
32
45
 
33
46
  function getOption(args, name) {
34
47
  const direct = args.find((arg) => arg.startsWith(`${name}=`));
@@ -90,6 +103,8 @@ function printHelp() {
90
103
  " da-vinci status",
91
104
  " da-vinci validate-assets",
92
105
  " da-vinci audit [project-path]",
106
+ " da-vinci icon-sync [--output <path>] [--timeout-ms <value>] [--strict]",
107
+ " da-vinci icon-search --query <text> [--family <value>] [--top <value>] [--catalog <path>] [--aliases <path>] [--json]",
93
108
  " da-vinci preflight-pencil --ops-file <path>",
94
109
  " da-vinci ensure-pen --output <path>",
95
110
  " da-vinci write-pen --output <path> --nodes-file <path> [--variables-file <path>]",
@@ -108,6 +123,14 @@ function printHelp() {
108
123
  " --platform <value> codex, claude, gemini, or all",
109
124
  " --home <path> override HOME for installation targets",
110
125
  " --project <path> override project path for audit",
126
+ " --catalog <path> icon catalog path for icon-search/icon-sync (default: ~/.da-vinci/icon-catalog.json)",
127
+ " --aliases <path> icon alias mapping file for icon-search (default: ~/.da-vinci/icon-aliases.json)",
128
+ " --query <text> icon-search query text",
129
+ " --family <value> icon-search family filter: all, material, rounded, outlined, sharp, lucide, feather, phosphor",
130
+ " --top <value> icon-search result count (1-50, default 8)",
131
+ " --timeout-ms <value> network timeout for icon-sync requests",
132
+ " --strict fail icon-sync when any upstream source request fails",
133
+ " --json print structured JSON for icon-search",
111
134
  " --pen <path> registered .pen path for sync checks",
112
135
  " --ops-file <path> Pencil batch operations file for preflight",
113
136
  " --input <path> input .pen file for snapshot-pen",
@@ -128,7 +151,28 @@ function printHelp() {
128
151
  async function runCli(argv) {
129
152
  const [command] = argv;
130
153
  const homeDir = getOption(argv, "--home");
131
- const positionalArgs = getPositionalArgs(argv.slice(1), ["--home", "--platform", "--project", "--mode", "--change"]);
154
+ const positionalArgs = getPositionalArgs(argv.slice(1), [
155
+ "--home",
156
+ "--platform",
157
+ "--project",
158
+ "--mode",
159
+ "--change",
160
+ "--query",
161
+ "--family",
162
+ "--top",
163
+ "--catalog",
164
+ "--aliases",
165
+ "--timeout-ms",
166
+ "--ops-file",
167
+ "--input",
168
+ "--output",
169
+ "--pen",
170
+ "--nodes-file",
171
+ "--variables-file",
172
+ "--version",
173
+ "--owner",
174
+ "--wait-ms"
175
+ ]);
132
176
 
133
177
  if (!command || command === "help" || command === "--help" || command === "-h") {
134
178
  printHelp();
@@ -184,6 +228,149 @@ async function runCli(argv) {
184
228
  return;
185
229
  }
186
230
 
231
+ if (command === "icon-sync") {
232
+ const outputPath = getOption(argv, "--output") || getOption(argv, "--catalog");
233
+ const timeoutMs = getOption(argv, "--timeout-ms");
234
+ const strict = argv.includes("--strict");
235
+
236
+ const result = await syncIconCatalog({
237
+ outputPath,
238
+ timeoutMs,
239
+ strict,
240
+ homeDir
241
+ });
242
+
243
+ console.log(formatIconSyncReport(result));
244
+ return;
245
+ }
246
+
247
+ if (command === "icon-search") {
248
+ const family = getOption(argv, "--family") || "all";
249
+ const topRaw = getOption(argv, "--top");
250
+ const queryOption = getOption(argv, "--query");
251
+ const catalogPath = getOption(argv, "--catalog");
252
+ const aliasesPath = getOption(argv, "--aliases");
253
+ const iconPositional = getPositionalArgs(argv.slice(1), [
254
+ "--home",
255
+ "--query",
256
+ "--family",
257
+ "--top",
258
+ "--catalog",
259
+ "--aliases"
260
+ ]);
261
+ const query = queryOption || iconPositional.join(" ").trim();
262
+
263
+ if (!query) {
264
+ throw new Error("`icon-search` requires `--query <text>` or positional query text.");
265
+ }
266
+
267
+ let top;
268
+ if (topRaw !== undefined) {
269
+ top = Number.parseInt(topRaw, 10);
270
+ if (!Number.isFinite(top) || top < 1 || top > 50) {
271
+ throw new Error("`icon-search --top` must be an integer between 1 and 50.");
272
+ }
273
+ }
274
+
275
+ let loadedCatalog = null;
276
+ let loadedCatalogPath = null;
277
+ let catalogLoadError = null;
278
+ let loadedAliases = null;
279
+ let loadedAliasesPath = null;
280
+ let aliasesLoadError = null;
281
+ let aliasExpansion = {
282
+ extraTokens: [],
283
+ matchedAliases: []
284
+ };
285
+
286
+ try {
287
+ const loaded = loadIconCatalog({
288
+ catalogPath,
289
+ homeDir
290
+ });
291
+ loadedCatalog = loaded.catalog;
292
+ loadedCatalogPath = loaded.catalogPath;
293
+ } catch (error) {
294
+ catalogLoadError = error.message || String(error);
295
+ }
296
+
297
+ try {
298
+ const loaded = loadIconAliases({
299
+ aliasPath: aliasesPath,
300
+ homeDir
301
+ });
302
+ loadedAliases = loaded;
303
+ loadedAliasesPath = loaded.aliasPath;
304
+ aliasExpansion = expandQueryWithAliases(query, loaded.aliases);
305
+ } catch (error) {
306
+ aliasesLoadError = error.message || String(error);
307
+ }
308
+
309
+ const result = searchIconLibrary(query, {
310
+ family,
311
+ top,
312
+ catalog: loadedCatalog ? loadedCatalog.icons : [],
313
+ extraQueryTokens: aliasExpansion.extraTokens
314
+ });
315
+ const jsonOutput = argv.includes("--json");
316
+
317
+ const resultWithMeta = {
318
+ ...result,
319
+ catalog: {
320
+ path: loadedCatalogPath || "(unresolved)",
321
+ loaded: Boolean(loadedCatalog),
322
+ iconCount: loadedCatalog ? loadedCatalog.iconCount : 0,
323
+ generatedAt: loadedCatalog ? loadedCatalog.generatedAt : null,
324
+ error: catalogLoadError
325
+ },
326
+ aliases: {
327
+ path: loadedAliasesPath || "(unresolved)",
328
+ loaded: loadedAliases ? Boolean(loadedAliases.loaded) : false,
329
+ available: Boolean(loadedAliases),
330
+ source: loadedAliases ? loadedAliases.source : null,
331
+ matched: aliasExpansion.matchedAliases.length,
332
+ extraTokens: aliasExpansion.extraTokens,
333
+ error: aliasesLoadError
334
+ }
335
+ };
336
+
337
+ if (jsonOutput) {
338
+ console.log(JSON.stringify(resultWithMeta, null, 2));
339
+ return;
340
+ }
341
+
342
+ if (resultWithMeta.catalog.loaded) {
343
+ console.log(
344
+ `Icon catalog: ${resultWithMeta.catalog.path} (${resultWithMeta.catalog.iconCount} icons, ${resultWithMeta.catalog.generatedAt})`
345
+ );
346
+ } else if (resultWithMeta.catalog.error) {
347
+ console.log(
348
+ `Icon catalog: ${resultWithMeta.catalog.path} (load failed: ${resultWithMeta.catalog.error}; using built-in fallback index)`
349
+ );
350
+ } else {
351
+ console.log(
352
+ `Icon catalog: ${resultWithMeta.catalog.path} (not found; using built-in fallback index; run \`da-vinci icon-sync\`)`
353
+ );
354
+ }
355
+
356
+ if (resultWithMeta.aliases.available) {
357
+ console.log(
358
+ `Icon aliases: ${resultWithMeta.aliases.path} (${resultWithMeta.aliases.source}, matched ${resultWithMeta.aliases.matched})`
359
+ );
360
+ } else if (resultWithMeta.aliases.error) {
361
+ console.log(
362
+ `Icon aliases: ${resultWithMeta.aliases.path} (load failed: ${resultWithMeta.aliases.error})`
363
+ );
364
+ } else {
365
+ console.log(
366
+ `Icon aliases: ${resultWithMeta.aliases.path} (not found; using built-in defaults only)`
367
+ );
368
+ }
369
+
370
+ console.log(formatIconSearchReport(result));
371
+ return;
372
+ }
373
+
187
374
  if (command === "preflight-pencil") {
188
375
  const opsFile = getOption(argv, "--ops-file");
189
376
  let operations = "";
@@ -0,0 +1,116 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const DEFAULT_MAX_DEPTH = 32;
5
+ const DEFAULT_MAX_ENTRIES = 20000;
6
+
7
+ function pathExists(targetPath) {
8
+ return fs.existsSync(targetPath);
9
+ }
10
+
11
+ function toPositiveInteger(value, fallback) {
12
+ const parsed = Number(value);
13
+ if (!Number.isFinite(parsed)) {
14
+ return fallback;
15
+ }
16
+
17
+ const normalized = Math.floor(parsed);
18
+ if (normalized <= 0) {
19
+ return fallback;
20
+ }
21
+
22
+ return normalized;
23
+ }
24
+
25
+ function isPathInside(basePath, targetPath) {
26
+ const resolvedBase = path.resolve(basePath);
27
+ const resolvedTarget = path.resolve(targetPath);
28
+
29
+ if (resolvedBase === resolvedTarget) {
30
+ return true;
31
+ }
32
+
33
+ const relative = path.relative(resolvedBase, resolvedTarget);
34
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
35
+ }
36
+
37
+ function listFilesRecursiveSafe(rootDir, options = {}) {
38
+ const maxDepth = toPositiveInteger(options.maxDepth, DEFAULT_MAX_DEPTH);
39
+ const maxEntries = toPositiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
40
+ const includeDotfiles = options.includeDotfiles !== false;
41
+ const skipSymlinks = options.skipSymlinks !== false;
42
+ const files = [];
43
+ const readErrors = [];
44
+
45
+ const summary = {
46
+ files,
47
+ truncated: false,
48
+ maxDepth,
49
+ maxEntries,
50
+ depthLimitHits: 0,
51
+ entryLimitHit: false,
52
+ skippedSymlinks: 0,
53
+ readErrors
54
+ };
55
+
56
+ if (!pathExists(rootDir)) {
57
+ return summary;
58
+ }
59
+
60
+ function walk(currentPath, depth) {
61
+ if (summary.entryLimitHit) {
62
+ return;
63
+ }
64
+
65
+ if (depth > maxDepth) {
66
+ summary.truncated = true;
67
+ summary.depthLimitHits += 1;
68
+ return;
69
+ }
70
+
71
+ let entries = [];
72
+ try {
73
+ entries = fs.readdirSync(currentPath, { withFileTypes: true });
74
+ } catch (error) {
75
+ readErrors.push({
76
+ path: currentPath,
77
+ error: error && error.message ? error.message : String(error)
78
+ });
79
+ return;
80
+ }
81
+
82
+ for (const entry of entries) {
83
+ if (!includeDotfiles && entry.name.startsWith(".")) {
84
+ continue;
85
+ }
86
+
87
+ const fullPath = path.join(currentPath, entry.name);
88
+ if (skipSymlinks && entry.isSymbolicLink()) {
89
+ summary.skippedSymlinks += 1;
90
+ continue;
91
+ }
92
+
93
+ if (entry.isDirectory()) {
94
+ walk(fullPath, depth + 1);
95
+ } else {
96
+ files.push(fullPath);
97
+ }
98
+
99
+ if (files.length >= maxEntries) {
100
+ summary.truncated = true;
101
+ summary.entryLimitHit = true;
102
+ break;
103
+ }
104
+ }
105
+ }
106
+
107
+ walk(path.resolve(rootDir), 0);
108
+ return summary;
109
+ }
110
+
111
+ module.exports = {
112
+ DEFAULT_MAX_DEPTH,
113
+ DEFAULT_MAX_ENTRIES,
114
+ isPathInside,
115
+ listFilesRecursiveSafe
116
+ };
@@ -0,0 +1,147 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const { normalizeIconText, tokenizeIconText } = require("./icon-text");
5
+
6
+ const DEFAULT_ALIASES = {
7
+ "保险箱": ["vault", "safe box", "archive", "inventory_2"],
8
+ "金库": ["vault", "safe box", "lock", "inventory_2"],
9
+ "解密": ["unlock", "lock_open", "key", "verified_user"],
10
+ "加密": ["lock", "shield", "key", "fingerprint"],
11
+ "重试": ["refresh", "retry", "sync", "rotate-cw", "arrow-clockwise"],
12
+ "警告": ["warning", "alert-triangle", "triangle-alert"],
13
+ "错误": ["error", "x-circle", "circle-x"],
14
+ "成功": ["check_circle", "check-circle", "circle-check"],
15
+ "设置": ["settings", "tune", "sliders", "sliders-horizontal"],
16
+ "首页": ["home", "house"],
17
+ "用户": ["person", "user", "account", "profile"],
18
+ "账户": ["person", "user", "account", "profile"],
19
+ "通知": ["notifications", "bell"],
20
+ "上传": ["upload", "cloud_upload", "cloud-upload", "upload-cloud"],
21
+ "下载": ["download", "cloud_download", "cloud-download", "download-cloud"],
22
+ "搜索": ["search", "magnifying-glass"],
23
+ "返回": ["chevron_left", "chevron-left", "caret-left", "arrow-left"],
24
+ "关闭": ["close", "x", "x-circle"],
25
+ "删除": ["delete", "trash", "trash-2"],
26
+ "确认": ["check", "done", "check_circle", "check-circle"],
27
+ "客服": ["headset", "support_agent", "message-circle", "chat"],
28
+ "客服消息": ["headset", "support_agent", "message-circle", "chat"]
29
+ };
30
+
31
+ const normalize = normalizeIconText;
32
+ const tokenize = tokenizeIconText;
33
+
34
+ function unique(values) {
35
+ return Array.from(new Set((values || []).filter(Boolean)));
36
+ }
37
+
38
+ function getDefaultAliasPath(homeDir) {
39
+ const root = homeDir ? path.resolve(homeDir) : os.homedir();
40
+ return path.join(root, ".da-vinci", "icon-aliases.json");
41
+ }
42
+
43
+ function resolveAliasPath(options = {}) {
44
+ if (options.aliasPath) {
45
+ return path.resolve(options.aliasPath);
46
+ }
47
+ return path.resolve(getDefaultAliasPath(options.homeDir));
48
+ }
49
+
50
+ function normalizeAliasMap(rawMap) {
51
+ const normalized = {};
52
+ for (const [key, values] of Object.entries(rawMap || {})) {
53
+ const normalizedKey = normalize(key);
54
+ if (!normalizedKey) {
55
+ continue;
56
+ }
57
+ const normalizedValues = unique(
58
+ (Array.isArray(values) ? values : [values]).flatMap((value) =>
59
+ String(value || "")
60
+ .split(",")
61
+ .map((entry) => entry.trim())
62
+ )
63
+ );
64
+ if (normalizedValues.length === 0) {
65
+ continue;
66
+ }
67
+ normalized[normalizedKey] = normalizedValues;
68
+ }
69
+ return normalized;
70
+ }
71
+
72
+ function loadIconAliases(options = {}) {
73
+ const aliasPath = resolveAliasPath(options);
74
+ const mergedBase = normalizeAliasMap(DEFAULT_ALIASES);
75
+
76
+ if (!fs.existsSync(aliasPath)) {
77
+ return {
78
+ aliasPath,
79
+ aliases: mergedBase,
80
+ loaded: false,
81
+ source: "default"
82
+ };
83
+ }
84
+
85
+ const text = fs.readFileSync(aliasPath, "utf8");
86
+ const parsed = JSON.parse(text);
87
+ const userAliases = normalizeAliasMap(parsed.aliases || parsed);
88
+
89
+ return {
90
+ aliasPath,
91
+ aliases: {
92
+ ...mergedBase,
93
+ ...userAliases
94
+ },
95
+ loaded: true,
96
+ source: "file"
97
+ };
98
+ }
99
+
100
+ function expandQueryWithAliases(query, aliases) {
101
+ const normalizedQuery = normalize(query);
102
+ const queryTokens = tokenize(normalizedQuery);
103
+ const matched = new Map();
104
+
105
+ if (!normalizedQuery) {
106
+ return {
107
+ extraTokens: [],
108
+ matchedAliases: []
109
+ };
110
+ }
111
+
112
+ for (const [aliasKey, aliasValues] of Object.entries(aliases || {})) {
113
+ if (!aliasKey || !Array.isArray(aliasValues) || aliasValues.length === 0) {
114
+ continue;
115
+ }
116
+
117
+ const tokenHit = queryTokens.includes(aliasKey);
118
+ const phraseHit = normalizedQuery === aliasKey || normalizedQuery.includes(aliasKey);
119
+ if (!tokenHit && !phraseHit) {
120
+ continue;
121
+ }
122
+ matched.set(aliasKey, aliasValues);
123
+ }
124
+
125
+ const extraTokens = unique(
126
+ Array.from(matched.values()).flatMap((aliasValues) =>
127
+ aliasValues.flatMap((value) => tokenize(value))
128
+ )
129
+ );
130
+
131
+ return {
132
+ extraTokens,
133
+ matchedAliases: Array.from(matched.entries()).map(([key, values]) => ({
134
+ key,
135
+ values
136
+ }))
137
+ };
138
+ }
139
+
140
+ module.exports = {
141
+ DEFAULT_ALIASES,
142
+ getDefaultAliasPath,
143
+ resolveAliasPath,
144
+ loadIconAliases,
145
+ expandQueryWithAliases,
146
+ normalizeAliasMap
147
+ };