jig-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +115 -0
- package/dist/index.js +544 -0
- package/package.json +61 -0
- package/rules/00-anti-patterns.md +501 -0
- package/rules/01-modes.md +213 -0
- package/rules/02-tokens.md +210 -0
- package/rules/03-patterns.md +459 -0
- package/rules/04-principles.md +154 -0
- package/rules/05-copy.md +153 -0
- package/rules.index.json +637 -0
- package/templates/SKILL.md.tmpl +53 -0
- package/templates/command-metadata.json +27 -0
- package/tokens/brand.default.css +152 -0
- package/tokens/mode.editorial.css +72 -0
- package/tokens/mode.operator.css +74 -0
- package/tokens/mode.product.css +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join as join5 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/paths.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { dirname, join, parse } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
var MARKERS = ["package.json", ".git", "jig.config.json"];
|
|
14
|
+
function findProjectRoot(startDir) {
|
|
15
|
+
let current = startDir;
|
|
16
|
+
const { root } = parse(startDir);
|
|
17
|
+
while (true) {
|
|
18
|
+
if (MARKERS.some((m) => existsSync(join(current, m)))) return current;
|
|
19
|
+
if (current === root) return startDir;
|
|
20
|
+
current = dirname(current);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function getPackageRoot() {
|
|
24
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
}
|
|
26
|
+
var ASSET_MARKER = "rules.index.json";
|
|
27
|
+
function assetRoot(startDir = getPackageRoot()) {
|
|
28
|
+
let current = startDir;
|
|
29
|
+
const { root } = parse(startDir);
|
|
30
|
+
while (true) {
|
|
31
|
+
if (existsSync(join(current, ASSET_MARKER))) return current;
|
|
32
|
+
if (current === root) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`assetRoot(): could not find an ancestor of "${startDir}" containing "${ASSET_MARKER}"`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
current = dirname(current);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/commands/install.ts
|
|
42
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
43
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
44
|
+
|
|
45
|
+
// src/adapters/types.ts
|
|
46
|
+
var SKILL_DESCRIPTION = "Design system rules for generating and reviewing UI. Load before building any interface.";
|
|
47
|
+
function assertSafeRelPath(relPath, adapterName) {
|
|
48
|
+
const isAbsolute = relPath.startsWith("/") || /^[A-Za-z]:/.test(relPath);
|
|
49
|
+
const hasDotDotSegment = relPath.split(/[\\/]/).includes("..");
|
|
50
|
+
if (isAbsolute || hasDotDotSegment) {
|
|
51
|
+
throw new Error(`Adapter '${adapterName}' produced an unsafe relPath: ${relPath}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function quoteYamlString(value) {
|
|
55
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
56
|
+
}
|
|
57
|
+
var BLOCK_START = "<!-- jig:start -->";
|
|
58
|
+
var BLOCK_END = "<!-- jig:end -->";
|
|
59
|
+
function agentsBlock(skillBody) {
|
|
60
|
+
return [BLOCK_START, "", "# Jig \u2014 UI rules", "", skillBody, "", BLOCK_END, ""].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/adapters/claude.ts
|
|
64
|
+
var claude = {
|
|
65
|
+
name: "claude",
|
|
66
|
+
displayName: "Claude Code",
|
|
67
|
+
supportsScope: () => true,
|
|
68
|
+
skillFiles(ctx) {
|
|
69
|
+
const content = [
|
|
70
|
+
"---",
|
|
71
|
+
"name: jig",
|
|
72
|
+
`description: ${quoteYamlString(SKILL_DESCRIPTION)}`,
|
|
73
|
+
"user-invocable: true",
|
|
74
|
+
"---",
|
|
75
|
+
"",
|
|
76
|
+
ctx.skillBody,
|
|
77
|
+
""
|
|
78
|
+
].join("\n");
|
|
79
|
+
return [{ relPath: ".claude/skills/jig/SKILL.md", content }];
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/adapters/codex.ts
|
|
84
|
+
var codex = {
|
|
85
|
+
name: "codex",
|
|
86
|
+
displayName: "Codex",
|
|
87
|
+
supportsScope: () => true,
|
|
88
|
+
skillFiles(ctx) {
|
|
89
|
+
const relPath = ctx.scope === "global" ? ".codex/AGENTS.md" : "AGENTS.md";
|
|
90
|
+
return [{ relPath, content: agentsBlock(ctx.skillBody) }];
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/adapters/cursor.ts
|
|
95
|
+
var cursor = {
|
|
96
|
+
name: "cursor",
|
|
97
|
+
displayName: "Cursor",
|
|
98
|
+
supportsScope: (scope) => scope === "project",
|
|
99
|
+
skillFiles(ctx) {
|
|
100
|
+
const content = [
|
|
101
|
+
"---",
|
|
102
|
+
`description: ${quoteYamlString(SKILL_DESCRIPTION)}`,
|
|
103
|
+
"alwaysApply: false",
|
|
104
|
+
"---",
|
|
105
|
+
"",
|
|
106
|
+
ctx.skillBody,
|
|
107
|
+
""
|
|
108
|
+
].join("\n");
|
|
109
|
+
return [{ relPath: ".cursor/rules/jig.mdc", content }];
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/adapters/opencode.ts
|
|
114
|
+
var opencode = {
|
|
115
|
+
name: "opencode",
|
|
116
|
+
displayName: "opencode",
|
|
117
|
+
supportsScope: () => true,
|
|
118
|
+
skillFiles(ctx) {
|
|
119
|
+
const content = [
|
|
120
|
+
"---",
|
|
121
|
+
"name: jig",
|
|
122
|
+
`description: ${quoteYamlString(SKILL_DESCRIPTION)}`,
|
|
123
|
+
"---",
|
|
124
|
+
"",
|
|
125
|
+
ctx.skillBody,
|
|
126
|
+
""
|
|
127
|
+
].join("\n");
|
|
128
|
+
const relPath = ctx.scope === "global" ? ".config/opencode/skills/jig/SKILL.md" : ".opencode/skills/jig/SKILL.md";
|
|
129
|
+
return [{ relPath, content }];
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/adapters/generic.ts
|
|
134
|
+
var generic = {
|
|
135
|
+
name: "generic",
|
|
136
|
+
displayName: "Generic (AGENTS.md)",
|
|
137
|
+
supportsScope: (scope) => scope === "project",
|
|
138
|
+
skillFiles(ctx) {
|
|
139
|
+
return [{ relPath: "AGENTS.md", content: agentsBlock(ctx.skillBody) }];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/adapters/registry.ts
|
|
144
|
+
var ADAPTERS = [claude, codex, cursor, opencode, generic];
|
|
145
|
+
function adapterNames() {
|
|
146
|
+
return ADAPTERS.map((a) => a.name);
|
|
147
|
+
}
|
|
148
|
+
function getAdapter(name) {
|
|
149
|
+
const found = ADAPTERS.find((a) => a.name === name);
|
|
150
|
+
if (!found) {
|
|
151
|
+
throw new Error(`Unknown agent '${name}'. Available: ${adapterNames().join(", ")}`);
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
function skillFilesFor(adapter, ctx) {
|
|
156
|
+
const files = adapter.skillFiles(ctx);
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
assertSafeRelPath(file.relPath, adapter.name);
|
|
159
|
+
}
|
|
160
|
+
return files;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/install/manifest.ts
|
|
164
|
+
import { createHash } from "crypto";
|
|
165
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
166
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
167
|
+
var MANIFEST_REL = join2(".jig", "manifest.json");
|
|
168
|
+
var REGENERATE_HINT = "Re-run 'jig install --agent <name>' to fix it.";
|
|
169
|
+
function checksum(content) {
|
|
170
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
171
|
+
return `sha256:${createHash("sha256").update(normalized, "utf8").digest("hex")}`;
|
|
172
|
+
}
|
|
173
|
+
function isPlainObject(value) {
|
|
174
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
function isSafeManifestKey(key) {
|
|
177
|
+
if (key.startsWith("/") || /^[A-Za-z]:/.test(key)) return false;
|
|
178
|
+
return !key.split(/[\\/]/).includes("..");
|
|
179
|
+
}
|
|
180
|
+
function validateManifest(raw, path) {
|
|
181
|
+
const fail = () => {
|
|
182
|
+
throw new Error(`Jig's manifest at ${path} is invalid or corrupted. ${REGENERATE_HINT}`);
|
|
183
|
+
};
|
|
184
|
+
if (!isPlainObject(raw)) fail();
|
|
185
|
+
const { version: version2, agent, scope, installedAt, files } = raw;
|
|
186
|
+
if (typeof version2 !== "string") fail();
|
|
187
|
+
if (typeof agent !== "string") fail();
|
|
188
|
+
if (typeof installedAt !== "string") fail();
|
|
189
|
+
if (scope !== "project" && scope !== "global") fail();
|
|
190
|
+
if (!isPlainObject(files)) fail();
|
|
191
|
+
for (const [key, value] of Object.entries(files)) {
|
|
192
|
+
if (typeof value !== "string") fail();
|
|
193
|
+
if (!isSafeManifestKey(key)) fail();
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
version: version2,
|
|
197
|
+
agent,
|
|
198
|
+
scope,
|
|
199
|
+
installedAt,
|
|
200
|
+
files
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function readManifest(projectRoot) {
|
|
204
|
+
const path = join2(projectRoot, MANIFEST_REL);
|
|
205
|
+
if (!existsSync2(path)) return null;
|
|
206
|
+
let raw;
|
|
207
|
+
try {
|
|
208
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error(`Jig's manifest at ${path} is not valid JSON. ${REGENERATE_HINT}`);
|
|
211
|
+
}
|
|
212
|
+
return validateManifest(raw, path);
|
|
213
|
+
}
|
|
214
|
+
function writeManifest(projectRoot, m) {
|
|
215
|
+
const path = join2(projectRoot, MANIFEST_REL);
|
|
216
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
217
|
+
writeFileSync(path, `${JSON.stringify(m, null, 2)}
|
|
218
|
+
`, "utf8");
|
|
219
|
+
}
|
|
220
|
+
function isModified(projectRoot, relPath, m) {
|
|
221
|
+
const recorded = m.files[relPath];
|
|
222
|
+
if (!recorded) return false;
|
|
223
|
+
const abs = join2(projectRoot, relPath);
|
|
224
|
+
if (!existsSync2(abs)) return false;
|
|
225
|
+
return checksum(readFileSync(abs, "utf8")) !== recorded;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/template/render.ts
|
|
229
|
+
var VAR = /\{\{([a-z_]+)\}\}/g;
|
|
230
|
+
function render(template, vars) {
|
|
231
|
+
return template.replace(VAR, (_match, name) => {
|
|
232
|
+
if (!(name in vars)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Template variable '{{${name}}}' has no value. Known variables: ${Object.keys(vars).join(", ")}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return vars[name];
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function escapeTableCell(value) {
|
|
241
|
+
return value.replace(/\|/g, "\\|");
|
|
242
|
+
}
|
|
243
|
+
function renderCommandTable(metadata) {
|
|
244
|
+
const rows = Object.entries(metadata).map(([name, meta]) => {
|
|
245
|
+
const signature = meta.argumentHint ? `${name} ${meta.argumentHint}` : name;
|
|
246
|
+
const status = meta.status === "planned" ? "planned \u2014 not yet implemented" : "available";
|
|
247
|
+
return `| \`${escapeTableCell(signature)}\` | ${escapeTableCell(meta.description)} | ${status} |`;
|
|
248
|
+
});
|
|
249
|
+
return ["| Command | Description | Status |", "| --- | --- | --- |", ...rows].join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/install/vendor.ts
|
|
253
|
+
function vendorHeader(file, version2) {
|
|
254
|
+
return [
|
|
255
|
+
`<!-- ${file} \u2014 vendored from Jig v${version2}.`,
|
|
256
|
+
" Licensed Apache-2.0. See .jig/LICENSE and .jig/NOTICE.",
|
|
257
|
+
" Edit freely: `jig update` will not overwrite a file you have changed. -->",
|
|
258
|
+
"",
|
|
259
|
+
""
|
|
260
|
+
].join("\n");
|
|
261
|
+
}
|
|
262
|
+
function findMarkerRegions(existing) {
|
|
263
|
+
const regions = [];
|
|
264
|
+
let searchFrom = 0;
|
|
265
|
+
let openStart = null;
|
|
266
|
+
while (true) {
|
|
267
|
+
const nextStart = existing.indexOf(BLOCK_START, searchFrom);
|
|
268
|
+
const nextEnd = existing.indexOf(BLOCK_END, searchFrom);
|
|
269
|
+
if (nextStart === -1 && nextEnd === -1) break;
|
|
270
|
+
const endIsNext = nextEnd !== -1 && (nextStart === -1 || nextEnd < nextStart);
|
|
271
|
+
if (endIsNext) {
|
|
272
|
+
if (openStart !== null) {
|
|
273
|
+
regions.push({ start: openStart, end: nextEnd + BLOCK_END.length });
|
|
274
|
+
openStart = null;
|
|
275
|
+
} else {
|
|
276
|
+
regions.push({ start: nextEnd, end: nextEnd + BLOCK_END.length });
|
|
277
|
+
}
|
|
278
|
+
searchFrom = nextEnd + BLOCK_END.length;
|
|
279
|
+
} else {
|
|
280
|
+
if (openStart !== null) {
|
|
281
|
+
regions.push({ start: openStart, end: openStart + BLOCK_START.length });
|
|
282
|
+
}
|
|
283
|
+
openStart = nextStart;
|
|
284
|
+
searchFrom = nextStart + BLOCK_START.length;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (openStart !== null) {
|
|
288
|
+
regions.push({ start: openStart, end: existing.length });
|
|
289
|
+
}
|
|
290
|
+
regions.sort((a, b) => a.start - b.start);
|
|
291
|
+
return regions;
|
|
292
|
+
}
|
|
293
|
+
function stripMarkerRegions(existing) {
|
|
294
|
+
const regions = findMarkerRegions(existing);
|
|
295
|
+
if (regions.length === 0) {
|
|
296
|
+
return { text: existing, insertAt: existing.length, removed: false };
|
|
297
|
+
}
|
|
298
|
+
let text = "";
|
|
299
|
+
let cursor2 = 0;
|
|
300
|
+
let insertAt = -1;
|
|
301
|
+
regions.forEach((region, i) => {
|
|
302
|
+
let chunk = existing.slice(cursor2, region.start);
|
|
303
|
+
if (i > 0) chunk = chunk.replace(/^\n/, "");
|
|
304
|
+
text += chunk;
|
|
305
|
+
if (insertAt === -1) insertAt = text.length;
|
|
306
|
+
cursor2 = region.end;
|
|
307
|
+
});
|
|
308
|
+
const tail = existing.slice(cursor2).replace(/^\n/, "");
|
|
309
|
+
text += tail;
|
|
310
|
+
return { text, insertAt, removed: true };
|
|
311
|
+
}
|
|
312
|
+
function upsertBlock(existing, block) {
|
|
313
|
+
const { text, insertAt, removed } = stripMarkerRegions(existing);
|
|
314
|
+
if (!removed) {
|
|
315
|
+
const separator = text.length && !text.endsWith("\n") ? "\n\n" : text.length ? "\n" : "";
|
|
316
|
+
return `${text}${separator}${block}`;
|
|
317
|
+
}
|
|
318
|
+
return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/commands/install.ts
|
|
322
|
+
var ASK_INSTRUCTION = "Ask the user directly in chat, one question at a time, and wait for an answer before continuing.";
|
|
323
|
+
function relKey(...parts) {
|
|
324
|
+
return parts.join("/");
|
|
325
|
+
}
|
|
326
|
+
function writeFile(root, key, content, files) {
|
|
327
|
+
const abs = join3(root, ...key.split("/"));
|
|
328
|
+
mkdirSync2(dirname3(abs), { recursive: true });
|
|
329
|
+
writeFileSync2(abs, content, "utf8");
|
|
330
|
+
files[key] = checksum(content);
|
|
331
|
+
}
|
|
332
|
+
function buildSkillBody(packageRoot2, scope) {
|
|
333
|
+
const template = readFileSync2(join3(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
|
|
334
|
+
const metadata = JSON.parse(
|
|
335
|
+
readFileSync2(join3(packageRoot2, "templates", "command-metadata.json"), "utf8")
|
|
336
|
+
);
|
|
337
|
+
return render(template, {
|
|
338
|
+
command_prefix: "/jig ",
|
|
339
|
+
scripts_path: "npx jig-ui",
|
|
340
|
+
ask_instruction: ASK_INSTRUCTION,
|
|
341
|
+
available_commands: renderCommandTable(metadata),
|
|
342
|
+
config_file: "jig.config.json",
|
|
343
|
+
rules_path: scope === "global" ? "~/.jig" : ".jig"
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function install(opts) {
|
|
347
|
+
const adapter = getAdapter(opts.agent);
|
|
348
|
+
if (!adapter.supportsScope(opts.scope)) {
|
|
349
|
+
throw new Error(`Adapter '${adapter.name}' does not support ${opts.scope} scope.`);
|
|
350
|
+
}
|
|
351
|
+
const installRoot = opts.scope === "global" ? opts.homeDir : opts.projectRoot;
|
|
352
|
+
let existingManifest;
|
|
353
|
+
try {
|
|
354
|
+
existingManifest = readManifest(installRoot);
|
|
355
|
+
} catch {
|
|
356
|
+
existingManifest = null;
|
|
357
|
+
}
|
|
358
|
+
const planned = [];
|
|
359
|
+
const rulesDir = join3(opts.packageRoot, "rules");
|
|
360
|
+
for (const file of readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
361
|
+
const body = readFileSync2(join3(rulesDir, file), "utf8");
|
|
362
|
+
planned.push({ key: relKey(".jig", file), content: vendorHeader(file, opts.version) + body, checkable: true });
|
|
363
|
+
}
|
|
364
|
+
planned.push({
|
|
365
|
+
key: relKey(".jig", "rules.index.json"),
|
|
366
|
+
content: readFileSync2(join3(opts.packageRoot, "rules.index.json"), "utf8"),
|
|
367
|
+
checkable: true
|
|
368
|
+
});
|
|
369
|
+
for (const src of ["LICENSE", "NOTICE"]) {
|
|
370
|
+
planned.push({
|
|
371
|
+
key: relKey(".jig", src),
|
|
372
|
+
content: readFileSync2(join3(opts.packageRoot, src), "utf8"),
|
|
373
|
+
checkable: false
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const skillBody = buildSkillBody(opts.packageRoot, opts.scope);
|
|
377
|
+
const skillFiles = skillFilesFor(adapter, {
|
|
378
|
+
version: opts.version,
|
|
379
|
+
scope: opts.scope,
|
|
380
|
+
skillBody,
|
|
381
|
+
commandPrefix: "/jig "
|
|
382
|
+
});
|
|
383
|
+
for (const file of skillFiles) {
|
|
384
|
+
const key = file.relPath;
|
|
385
|
+
const abs = join3(installRoot, ...key.split("/"));
|
|
386
|
+
const isBlockFile = file.content.includes(BLOCK_START);
|
|
387
|
+
const content = isBlockFile && existsSync3(abs) ? upsertBlock(readFileSync2(abs, "utf8"), file.content) : file.content;
|
|
388
|
+
planned.push({ key, content, checkable: !isBlockFile });
|
|
389
|
+
}
|
|
390
|
+
const files = {};
|
|
391
|
+
const written = [];
|
|
392
|
+
const skipped = [];
|
|
393
|
+
for (const { key, content, checkable } of planned) {
|
|
394
|
+
if (existingManifest && checkable && isModified(installRoot, key, existingManifest)) {
|
|
395
|
+
skipped.push(key);
|
|
396
|
+
const recorded = existingManifest.files[key];
|
|
397
|
+
if (recorded) files[key] = recorded;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
writeFile(installRoot, key, content, files);
|
|
401
|
+
written.push(key);
|
|
402
|
+
}
|
|
403
|
+
const manifest = {
|
|
404
|
+
version: opts.version,
|
|
405
|
+
agent: opts.agent,
|
|
406
|
+
scope: opts.scope,
|
|
407
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
408
|
+
files
|
|
409
|
+
};
|
|
410
|
+
writeManifest(installRoot, manifest);
|
|
411
|
+
written.push(relKey(".jig", "manifest.json"));
|
|
412
|
+
return { written, skipped };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/commands/update.ts
|
|
416
|
+
import { basename, dirname as dirname4, join as join4, resolve } from "path";
|
|
417
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
418
|
+
var ALWAYS_REPLACE = [relKey(".jig", "LICENSE"), relKey(".jig", "NOTICE")];
|
|
419
|
+
function update(opts) {
|
|
420
|
+
const projectManifest = readManifest(opts.projectRoot);
|
|
421
|
+
const existing = projectManifest ?? readManifest(opts.homeDir);
|
|
422
|
+
if (!existing) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Jig is not installed in ${opts.projectRoot}. Run 'jig install --agent <name>' first.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const sameRoot = resolve(opts.projectRoot) === resolve(opts.homeDir);
|
|
428
|
+
const discoveredScope = !projectManifest ? "global" : sameRoot ? existing.scope : "project";
|
|
429
|
+
const installRoot = discoveredScope === "global" ? opts.homeDir : opts.projectRoot;
|
|
430
|
+
const adapter = getAdapter(existing.agent);
|
|
431
|
+
if (!adapter.supportsScope(discoveredScope)) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Jig's manifest at ${installRoot} records agent '${existing.agent}', which does not support ${discoveredScope} scope. Re-run 'jig install --agent <name>' to fix it.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const updated = [];
|
|
437
|
+
const skipped = [];
|
|
438
|
+
const files = { ...existing.files };
|
|
439
|
+
const write = (key, content) => {
|
|
440
|
+
const abs = join4(installRoot, ...key.split("/"));
|
|
441
|
+
mkdirSync3(dirname4(abs), { recursive: true });
|
|
442
|
+
writeFileSync3(abs, content, "utf8");
|
|
443
|
+
files[key] = checksum(content);
|
|
444
|
+
updated.push(key);
|
|
445
|
+
};
|
|
446
|
+
const rulesDir = join4(opts.packageRoot, "rules");
|
|
447
|
+
for (const file of readdirSync2(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
448
|
+
const key = relKey(".jig", file);
|
|
449
|
+
if (isModified(installRoot, key, existing)) {
|
|
450
|
+
skipped.push(key);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
write(key, vendorHeader(file, opts.version) + readFileSync3(join4(rulesDir, file), "utf8"));
|
|
454
|
+
}
|
|
455
|
+
const indexKey = relKey(".jig", "rules.index.json");
|
|
456
|
+
if (isModified(installRoot, indexKey, existing)) {
|
|
457
|
+
skipped.push(indexKey);
|
|
458
|
+
} else {
|
|
459
|
+
write(indexKey, readFileSync3(join4(opts.packageRoot, "rules.index.json"), "utf8"));
|
|
460
|
+
}
|
|
461
|
+
const skillBody = buildSkillBody(opts.packageRoot, discoveredScope);
|
|
462
|
+
const skillFiles = skillFilesFor(adapter, {
|
|
463
|
+
version: opts.version,
|
|
464
|
+
scope: discoveredScope,
|
|
465
|
+
skillBody,
|
|
466
|
+
commandPrefix: "/jig "
|
|
467
|
+
});
|
|
468
|
+
for (const file of skillFiles) {
|
|
469
|
+
const key = file.relPath;
|
|
470
|
+
const isBlockFile = file.content.includes(BLOCK_START);
|
|
471
|
+
if (isBlockFile) {
|
|
472
|
+
const abs = join4(installRoot, ...key.split("/"));
|
|
473
|
+
const current = existsSync4(abs) ? readFileSync3(abs, "utf8") : "";
|
|
474
|
+
write(key, upsertBlock(current, file.content));
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (isModified(installRoot, key, existing)) {
|
|
478
|
+
skipped.push(key);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
write(key, file.content);
|
|
482
|
+
}
|
|
483
|
+
for (const key of ALWAYS_REPLACE) {
|
|
484
|
+
write(key, readFileSync3(join4(opts.packageRoot, basename(key)), "utf8"));
|
|
485
|
+
}
|
|
486
|
+
const manifest = {
|
|
487
|
+
...existing,
|
|
488
|
+
scope: discoveredScope,
|
|
489
|
+
version: opts.version,
|
|
490
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
491
|
+
files
|
|
492
|
+
};
|
|
493
|
+
writeManifest(installRoot, manifest);
|
|
494
|
+
return { updated, skipped, fromVersion: existing.version, toVersion: opts.version };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/index.ts
|
|
498
|
+
var packageRoot = getPackageRoot();
|
|
499
|
+
var { version } = JSON.parse(readFileSync4(join5(packageRoot, "package.json"), "utf8"));
|
|
500
|
+
var program = new Command();
|
|
501
|
+
program.name("jig").description("A design system for coding agents.").version(version);
|
|
502
|
+
program.command("install").description("Install Jig rules and the agent skill file into a repository.").requiredOption("--agent <name>", `target agent (${adapterNames().join(", ")})`).option("--scope <scope>", "project or global", "project").action((opts) => {
|
|
503
|
+
if (opts.scope !== "project" && opts.scope !== "global") {
|
|
504
|
+
console.error(`Invalid scope '${opts.scope}'. Use 'project' or 'global'.`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
508
|
+
try {
|
|
509
|
+
const result = install({
|
|
510
|
+
agent: opts.agent,
|
|
511
|
+
scope: opts.scope,
|
|
512
|
+
projectRoot,
|
|
513
|
+
packageRoot: assetRoot(),
|
|
514
|
+
version,
|
|
515
|
+
homeDir: homedir()
|
|
516
|
+
});
|
|
517
|
+
console.log(`Installed Jig v${version} for ${opts.agent} (${opts.scope} scope)`);
|
|
518
|
+
for (const f of result.written) console.log(` + ${f}`);
|
|
519
|
+
for (const f of result.skipped) console.log(` \xB7 ${f} (edited locally, left alone)`);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
console.error(err.message);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
program.command("update").description("Update vendored Jig rules, skipping files you have edited.").action(() => {
|
|
526
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
527
|
+
try {
|
|
528
|
+
const result = update({
|
|
529
|
+
agent: "",
|
|
530
|
+
scope: "project",
|
|
531
|
+
projectRoot,
|
|
532
|
+
packageRoot: assetRoot(),
|
|
533
|
+
version,
|
|
534
|
+
homeDir: homedir()
|
|
535
|
+
});
|
|
536
|
+
console.log(`Updated Jig ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
537
|
+
for (const f of result.updated) console.log(` ~ ${f}`);
|
|
538
|
+
for (const f of result.skipped) console.log(` \xB7 ${f} (edited locally, left alone)`);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.error(err.message);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jig-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A design system for coding agents. 104 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jig": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"rules",
|
|
13
|
+
"tokens",
|
|
14
|
+
"templates",
|
|
15
|
+
"rules.index.json",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "tsc --noEmit && vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"prepack": "npm run build && node scripts/stage-assets.mjs"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"tsup": "^8.3.0",
|
|
36
|
+
"typescript": "^5.6.0",
|
|
37
|
+
"vitest": "^2.1.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"design-system",
|
|
41
|
+
"design-tokens",
|
|
42
|
+
"ai-agents",
|
|
43
|
+
"claude-code",
|
|
44
|
+
"codex",
|
|
45
|
+
"cursor",
|
|
46
|
+
"opencode",
|
|
47
|
+
"ui",
|
|
48
|
+
"accessibility",
|
|
49
|
+
"css"
|
|
50
|
+
],
|
|
51
|
+
"author": "timessuman",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/timessuman/jig.git",
|
|
55
|
+
"directory": "packages/cli"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/timessuman/jig#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/timessuman/jig/issues"
|
|
60
|
+
}
|
|
61
|
+
}
|