@captain_z/zsk 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.
Files changed (49) hide show
  1. package/README.md +147 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +155 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/commands/add.d.ts +13 -0
  6. package/dist/commands/add.js +418 -0
  7. package/dist/commands/add.js.map +1 -0
  8. package/dist/commands/init.d.ts +1 -0
  9. package/dist/commands/init.js +127 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/install.d.ts +7 -0
  12. package/dist/commands/install.js +54 -0
  13. package/dist/commands/install.js.map +1 -0
  14. package/dist/commands/project-init.d.ts +6 -0
  15. package/dist/commands/project-init.js +63 -0
  16. package/dist/commands/project-init.js.map +1 -0
  17. package/dist/commands/remove.d.ts +11 -0
  18. package/dist/commands/remove.js +209 -0
  19. package/dist/commands/remove.js.map +1 -0
  20. package/dist/core/bundles.d.ts +7 -0
  21. package/dist/core/bundles.js +39 -0
  22. package/dist/core/bundles.js.map +1 -0
  23. package/dist/core/capabilities.d.ts +25 -0
  24. package/dist/core/capabilities.js +79 -0
  25. package/dist/core/capabilities.js.map +1 -0
  26. package/dist/core/installer.d.ts +18 -0
  27. package/dist/core/installer.js +114 -0
  28. package/dist/core/installer.js.map +1 -0
  29. package/dist/core/manifest.d.ts +39 -0
  30. package/dist/core/manifest.js +89 -0
  31. package/dist/core/manifest.js.map +1 -0
  32. package/dist/core/scaffolder.d.ts +14 -0
  33. package/dist/core/scaffolder.js +71 -0
  34. package/dist/core/scaffolder.js.map +1 -0
  35. package/dist/core/skills.d.ts +17 -0
  36. package/dist/core/skills.js +89 -0
  37. package/dist/core/skills.js.map +1 -0
  38. package/dist/core/targets.d.ts +9 -0
  39. package/dist/core/targets.js +50 -0
  40. package/dist/core/targets.js.map +1 -0
  41. package/dist/ui/prompts.d.ts +10 -0
  42. package/dist/ui/prompts.js +40 -0
  43. package/dist/ui/prompts.js.map +1 -0
  44. package/package.json +39 -0
  45. package/templates/project-init/.raws/FIGMA-INDEX.md +38 -0
  46. package/templates/project-init/.raws/README.md +37 -0
  47. package/templates/project-init/.raws/SRS.md +73 -0
  48. package/templates/project-init/CLAUDE.md +45 -0
  49. package/templates/project-init/project-config.md +296 -0
@@ -0,0 +1,39 @@
1
+ export declare const MANIFEST_FILENAME = "zsk-manifest.json";
2
+ export declare const MANIFEST_SCHEMA_VERSION = 1;
3
+ export type ManifestSkill = {
4
+ name: string;
5
+ version: string;
6
+ sha256: string;
7
+ relPath: string;
8
+ };
9
+ export type Manifest = {
10
+ $schema: string;
11
+ version: typeof MANIFEST_SCHEMA_VERSION;
12
+ zskVersion: string;
13
+ installedAt: string;
14
+ bundle: string | null;
15
+ skills: ManifestSkill[];
16
+ };
17
+ export type DriftKind = "clean" | "modified" | "missing";
18
+ export type DriftEntry = {
19
+ name: string;
20
+ relPath: string;
21
+ kind: DriftKind;
22
+ };
23
+ export declare function readManifest(target: string): Promise<Manifest | null>;
24
+ export declare function writeManifest(target: string, manifest: Manifest): Promise<void>;
25
+ export declare function sha256File(content: Buffer | string): string;
26
+ export declare function buildManifest(params: {
27
+ zskVersion: string;
28
+ bundle: string | null;
29
+ skills: ManifestSkill[];
30
+ }): Manifest;
31
+ /**
32
+ * Compare manifest-recorded skills against on-disk files.
33
+ * Returns per-skill drift classification:
34
+ * clean — disk matches manifest sha256
35
+ * modified — file exists but sha256 differs (hand-edited)
36
+ * missing — file removed
37
+ */
38
+ export declare function detectDrift(target: string, manifest: Manifest): Promise<DriftEntry[]>;
39
+ export declare function hasManifest(target: string): Promise<boolean>;
@@ -0,0 +1,89 @@
1
+ // zsk-manifest.json — install-state contract for a target directory.
2
+ //
3
+ // Written by `zsk add`, consumed by `zsk add` (refresh path) / `zsk remove` /
4
+ // `zsk doctor`. Schema v1 is intentionally minimal; see ARCHITECTURE.md §6.4.4.
5
+ import { createHash } from "node:crypto";
6
+ import { readFile, writeFile, access } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ export const MANIFEST_FILENAME = "zsk-manifest.json";
9
+ export const MANIFEST_SCHEMA_VERSION = 1;
10
+ export async function readManifest(target) {
11
+ const p = join(target, MANIFEST_FILENAME);
12
+ try {
13
+ const raw = await readFile(p, "utf8");
14
+ const parsed = JSON.parse(raw);
15
+ if (parsed.version !== MANIFEST_SCHEMA_VERSION) {
16
+ throw new Error(`Manifest schema mismatch at ${p} (expected v${MANIFEST_SCHEMA_VERSION}, got v${parsed.version})`);
17
+ }
18
+ return parsed;
19
+ }
20
+ catch (err) {
21
+ if (isENOENT(err))
22
+ return null;
23
+ throw err;
24
+ }
25
+ }
26
+ export async function writeManifest(target, manifest) {
27
+ const p = join(target, MANIFEST_FILENAME);
28
+ const body = `${JSON.stringify(manifest, null, 2)}\n`;
29
+ await writeFile(p, body, "utf8");
30
+ }
31
+ export function sha256File(content) {
32
+ return createHash("sha256").update(content).digest("hex");
33
+ }
34
+ export function buildManifest(params) {
35
+ return {
36
+ $schema: "https://zsk.codeshareman.dev/manifest.v1.json",
37
+ version: MANIFEST_SCHEMA_VERSION,
38
+ zskVersion: params.zskVersion,
39
+ installedAt: new Date().toISOString(),
40
+ bundle: params.bundle,
41
+ skills: params.skills,
42
+ };
43
+ }
44
+ /**
45
+ * Compare manifest-recorded skills against on-disk files.
46
+ * Returns per-skill drift classification:
47
+ * clean — disk matches manifest sha256
48
+ * modified — file exists but sha256 differs (hand-edited)
49
+ * missing — file removed
50
+ */
51
+ export async function detectDrift(target, manifest) {
52
+ const out = [];
53
+ for (const s of manifest.skills) {
54
+ const p = join(target, s.relPath);
55
+ try {
56
+ const buf = await readFile(p);
57
+ const digest = sha256File(buf);
58
+ out.push({
59
+ name: s.name,
60
+ relPath: s.relPath,
61
+ kind: digest === s.sha256 ? "clean" : "modified",
62
+ });
63
+ }
64
+ catch (err) {
65
+ if (isENOENT(err)) {
66
+ out.push({ name: s.name, relPath: s.relPath, kind: "missing" });
67
+ }
68
+ else {
69
+ throw err;
70
+ }
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ export async function hasManifest(target) {
76
+ try {
77
+ await access(join(target, MANIFEST_FILENAME));
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ function isENOENT(err) {
85
+ return (typeof err === "object" &&
86
+ err !== null &&
87
+ err.code === "ENOENT");
88
+ }
89
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/core/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export type ScaffoldResult = {
2
+ written: string[];
3
+ skipped: string[];
4
+ };
5
+ export declare function resolveTemplatesRoot(): Promise<string>;
6
+ /**
7
+ * Recursively copy a template subtree into target dir.
8
+ * By default skips files that already exist (non-destructive).
9
+ */
10
+ export declare function scaffoldTemplate(templateSubpath: string, target: string, opts?: {
11
+ overwrite?: boolean;
12
+ }): Promise<ScaffoldResult>;
13
+ declare function fileExists(p: string): Promise<boolean>;
14
+ export { fileExists };
@@ -0,0 +1,71 @@
1
+ // Project scaffolder: copy templates into a target project directory.
2
+ // Used by `zsk project-init` to bootstrap project-config.md / .raws/ / CLAUDE.md.
3
+ import { readFile, writeFile, mkdir, readdir, access } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ let templatesRootCache = null;
8
+ export async function resolveTemplatesRoot() {
9
+ if (templatesRootCache)
10
+ return templatesRootCache;
11
+ // Dev (monorepo): packages/cli/src/core/scaffolder.ts → packages/cli/templates/
12
+ const thisDir = dirname(fileURLToPath(import.meta.url));
13
+ const devPath = resolve(thisDir, "../../templates");
14
+ try {
15
+ await access(devPath);
16
+ templatesRootCache = devPath;
17
+ return devPath;
18
+ }
19
+ catch {
20
+ // fall through
21
+ }
22
+ // Production: @captain_z/zsk/templates (bundled)
23
+ const require = createRequire(import.meta.url);
24
+ const pkgPath = require.resolve("@captain_z/zsk/package.json");
25
+ const templates = resolve(dirname(pkgPath), "templates");
26
+ templatesRootCache = templates;
27
+ return templates;
28
+ }
29
+ /**
30
+ * Recursively copy a template subtree into target dir.
31
+ * By default skips files that already exist (non-destructive).
32
+ */
33
+ export async function scaffoldTemplate(templateSubpath, target, opts = {}) {
34
+ const root = await resolveTemplatesRoot();
35
+ const src = join(root, templateSubpath);
36
+ const result = { written: [], skipped: [] };
37
+ await mkdir(target, { recursive: true });
38
+ await copyRecursive(src, target, opts.overwrite === true, result);
39
+ return result;
40
+ }
41
+ async function copyRecursive(src, dst, overwrite, result) {
42
+ const entries = await readdir(src, { withFileTypes: true });
43
+ await mkdir(dst, { recursive: true });
44
+ for (const e of entries) {
45
+ const srcPath = join(src, e.name);
46
+ const dstPath = join(dst, e.name);
47
+ if (e.isDirectory()) {
48
+ await copyRecursive(srcPath, dstPath, overwrite, result);
49
+ }
50
+ else {
51
+ if (!overwrite && (await fileExists(dstPath))) {
52
+ result.skipped.push(dstPath);
53
+ continue;
54
+ }
55
+ const content = await readFile(srcPath, "utf8");
56
+ await writeFile(dstPath, content, "utf8");
57
+ result.written.push(dstPath);
58
+ }
59
+ }
60
+ }
61
+ async function fileExists(p) {
62
+ try {
63
+ await access(p);
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ export { fileExists };
71
+ //# sourceMappingURL=scaffolder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffolder.js","sourceRoot":"","sources":["../../src/core/scaffolder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ declare const DOMAINS: readonly ["sdlc", "frontend", "quality", "design-handoff", "system", "meta"];
2
+ export type Domain = (typeof DOMAINS)[number];
3
+ export type Skill = {
4
+ name: string;
5
+ domain: Domain;
6
+ slug: string;
7
+ file: string;
8
+ };
9
+ export declare function resolveSkillsRoot(): Promise<string>;
10
+ export declare function loadSkill(skillName: string): Promise<Skill>;
11
+ /**
12
+ * Enumerate every installable skill by walking the skills root directly.
13
+ * Returns names in `zsk:<slug>` form, sorted by domain then slug, so UI
14
+ * groupings are stable across runs.
15
+ */
16
+ export declare function enumerateSkills(): Promise<Skill[]>;
17
+ export { DOMAINS };
@@ -0,0 +1,89 @@
1
+ // Skill resolution: zsk:<slug> → filesystem SKILL.md path + domain metadata.
2
+ //
3
+ // Dev (monorepo): resolves via ../../../../capabilities/skills/
4
+ // Production: resolves via @captain_z/zsk-skills/skills/
5
+ import { access, readdir } from "node:fs/promises";
6
+ import { dirname, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { createRequire } from "node:module";
9
+ const DOMAINS = [
10
+ "sdlc",
11
+ "frontend",
12
+ "quality",
13
+ "design-handoff",
14
+ "system",
15
+ "meta",
16
+ ];
17
+ let skillsRootCache = null;
18
+ export async function resolveSkillsRoot() {
19
+ if (skillsRootCache)
20
+ return skillsRootCache;
21
+ const thisDir = dirname(fileURLToPath(import.meta.url));
22
+ // Dev: packages/cli/src/core/skills.ts → capabilities/skills/
23
+ const devPath = resolve(thisDir, "../../../../capabilities/skills");
24
+ try {
25
+ await access(devPath);
26
+ skillsRootCache = devPath;
27
+ return devPath;
28
+ }
29
+ catch {
30
+ // fall through
31
+ }
32
+ // Production: @captain_z/zsk-skills package (flat layout, no /skills/ wrapper)
33
+ const require = createRequire(import.meta.url);
34
+ const pkgPath = require.resolve("@captain_z/zsk-skills/package.json");
35
+ const skillsDir = dirname(pkgPath);
36
+ skillsRootCache = skillsDir;
37
+ return skillsDir;
38
+ }
39
+ export async function loadSkill(skillName) {
40
+ const slug = skillName.replace(/^zsk:/, "");
41
+ const root = await resolveSkillsRoot();
42
+ for (const domain of DOMAINS) {
43
+ const file = resolve(root, domain, slug, "SKILL.md");
44
+ try {
45
+ await access(file);
46
+ return { name: skillName, domain, slug, file };
47
+ }
48
+ catch {
49
+ // try next domain
50
+ }
51
+ }
52
+ throw new Error(`Skill not found in any domain: ${skillName}`);
53
+ }
54
+ /**
55
+ * Enumerate every installable skill by walking the skills root directly.
56
+ * Returns names in `zsk:<slug>` form, sorted by domain then slug, so UI
57
+ * groupings are stable across runs.
58
+ */
59
+ export async function enumerateSkills() {
60
+ const root = await resolveSkillsRoot();
61
+ const out = [];
62
+ for (const domain of DOMAINS) {
63
+ const domainDir = resolve(root, domain);
64
+ let entries;
65
+ try {
66
+ entries = await readdir(domainDir, { withFileTypes: true });
67
+ }
68
+ catch {
69
+ continue; // domain dir may not exist in partial installs
70
+ }
71
+ for (const e of entries) {
72
+ if (!e.isDirectory())
73
+ continue;
74
+ const file = resolve(domainDir, e.name, "SKILL.md");
75
+ try {
76
+ await access(file);
77
+ }
78
+ catch {
79
+ continue; // directory without SKILL.md → not a skill
80
+ }
81
+ out.push({ name: `zsk:${e.name}`, domain, slug: e.name, file });
82
+ }
83
+ }
84
+ out.sort((a, b) => DOMAINS.indexOf(a.domain) - DOMAINS.indexOf(b.domain) ||
85
+ a.slug.localeCompare(b.slug));
86
+ return out;
87
+ }
88
+ export { DOMAINS };
89
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export type TargetKind = "claude-global" | "claude-project" | "claude-plugin" | "codex-global" | "codex-project" | "custom";
2
+ export type TargetOption = {
3
+ kind: TargetKind;
4
+ label: string;
5
+ hint: string;
6
+ resolve: () => string;
7
+ };
8
+ export declare const TARGET_OPTIONS: TargetOption[];
9
+ export declare function resolvePath(input: string): string;
@@ -0,0 +1,50 @@
1
+ // Install target resolution.
2
+ // Each TargetKind resolves to a concrete filesystem path.
3
+ import { homedir } from "node:os";
4
+ import { resolve, isAbsolute } from "node:path";
5
+ export const TARGET_OPTIONS = [
6
+ {
7
+ kind: "claude-global",
8
+ label: "~/.claude/skills/",
9
+ hint: "Claude 全局",
10
+ resolve: () => resolve(homedir(), ".claude/skills"),
11
+ },
12
+ {
13
+ kind: "claude-project",
14
+ label: "./.claude/skills/",
15
+ hint: "Claude 项目(当前目录)",
16
+ resolve: () => resolve(process.cwd(), ".claude/skills"),
17
+ },
18
+ {
19
+ kind: "claude-plugin",
20
+ label: "./.claude-plugin/",
21
+ hint: "Claude 插件形态(生成 manifest.json)",
22
+ resolve: () => resolve(process.cwd(), ".claude-plugin"),
23
+ },
24
+ {
25
+ kind: "codex-global",
26
+ label: "~/.codex/skills/",
27
+ hint: "Codex 全局",
28
+ resolve: () => resolve(homedir(), ".codex/skills"),
29
+ },
30
+ {
31
+ kind: "codex-project",
32
+ label: "./.codex/skills/",
33
+ hint: "Codex 项目",
34
+ resolve: () => resolve(process.cwd(), ".codex/skills"),
35
+ },
36
+ {
37
+ kind: "custom",
38
+ label: "自定义路径",
39
+ hint: "手动输入绝对或相对路径",
40
+ resolve: () => "",
41
+ },
42
+ ];
43
+ export function resolvePath(input) {
44
+ const trimmed = input.trim();
45
+ if (trimmed.startsWith("~/")) {
46
+ return resolve(homedir(), trimmed.slice(2));
47
+ }
48
+ return isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
49
+ }
50
+ //# sourceMappingURL=targets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targets.js","sourceRoot":"","sources":["../../src/core/targets.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,0DAA0D;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAiBhD,MAAM,CAAC,MAAM,cAAc,GAAmB;IAC5C;QACE,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,mBAAmB;QAC1B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,gBAAgB,CAAC;KACpD;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,mBAAmB;QAC1B,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,gBAAgB,CAAC;KACxD;IACD;QACE,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,mBAAmB;QAC1B,IAAI,EAAE,+BAA+B;QACrC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,gBAAgB,CAAC;KACxD;IACD;QACE,IAAI,EAAE,cAAc;QACpB,KAAK,EAAE,kBAAkB;QACzB,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,eAAe,CAAC;KACnD;IACD;QACE,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,kBAAkB;QACzB,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,CAAC;KACvD;IACD;QACE,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,OAAO;QACd,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE;KAClB;CACF,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;AACzE,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { intro as clackIntro, outro as clackOutro, spinner as clackSpinner, note as clackNote, log, type Option } from "@clack/prompts";
2
+ type Primitive = Readonly<string | boolean | number>;
3
+ export declare function select<T extends Primitive>(message: string, options: Option<T>[], initialValue?: T): Promise<T>;
4
+ export declare function groupMultiselect<T extends Primitive>(message: string, options: Record<string, Option<T>[]>, initialValues?: T[]): Promise<T[]>;
5
+ export declare function text(message: string, opts?: {
6
+ placeholder?: string;
7
+ validate?: (v: string) => string | undefined;
8
+ }): Promise<string>;
9
+ export declare function confirm(message: string, initialValue?: boolean): Promise<boolean>;
10
+ export { clackIntro as intro, clackOutro as outro, clackSpinner as spinner, clackNote as note, log, };
@@ -0,0 +1,40 @@
1
+ // @clack/prompts wrappers with centralized isCancel handling.
2
+ // All CLI interactive prompts go through this module so Ctrl+C is handled once.
3
+ import { intro as clackIntro, outro as clackOutro, select as clackSelect, groupMultiselect as clackGroupMultiselect, confirm as clackConfirm, text as clackText, spinner as clackSpinner, note as clackNote, log, cancel, isCancel, } from "@clack/prompts";
4
+ function exitIfCancel(value) {
5
+ if (isCancel(value)) {
6
+ cancel("已取消");
7
+ process.exit(0);
8
+ }
9
+ }
10
+ export async function select(message, options, initialValue) {
11
+ const v = await clackSelect({ message, options, initialValue });
12
+ exitIfCancel(v);
13
+ return v;
14
+ }
15
+ export async function groupMultiselect(message, options, initialValues = []) {
16
+ const v = await clackGroupMultiselect({
17
+ message,
18
+ options,
19
+ initialValues,
20
+ required: false,
21
+ });
22
+ exitIfCancel(v);
23
+ return v;
24
+ }
25
+ export async function text(message, opts = {}) {
26
+ const v = await clackText({
27
+ message,
28
+ placeholder: opts.placeholder,
29
+ validate: opts.validate,
30
+ });
31
+ exitIfCancel(v);
32
+ return v;
33
+ }
34
+ export async function confirm(message, initialValue = true) {
35
+ const v = await clackConfirm({ message, initialValue });
36
+ exitIfCancel(v);
37
+ return v;
38
+ }
39
+ export { clackIntro as intro, clackOutro as outro, clackSpinner as spinner, clackNote as note, log, };
40
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/ui/prompts.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,gFAAgF;AAEhF,OAAO,EACL,KAAK,IAAI,UAAU,EACnB,KAAK,IAAI,UAAU,EACnB,MAAM,IAAI,WAAW,EACrB,gBAAgB,IAAI,qBAAqB,EACzC,OAAO,IAAI,YAAY,EACvB,IAAI,IAAI,SAAS,EACjB,OAAO,IAAI,YAAY,EACvB,IAAI,IAAI,SAAS,EACjB,GAAG,EACH,MAAM,EACN,QAAQ,GAET,MAAM,gBAAgB,CAAC;AAIxB,SAAS,YAAY,CAAI,KAAiB;IACxC,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,CAAC,KAAK,CAAC,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,OAAe,EACf,OAAoB,EACpB,YAAgB;IAEhB,MAAM,CAAC,GAAG,MAAM,WAAW,CAAI,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;IACnE,YAAY,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAe,EACf,OAAoC,EACpC,gBAAqB,EAAE;IAEvB,MAAM,CAAC,GAAG,MAAM,qBAAqB,CAAI;QACvC,OAAO;QACP,OAAO;QACP,aAAa;QACb,QAAQ,EAAE,KAAK;KAChB,CAAC,CAAC;IACH,YAAY,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,OAAe,EACf,OAGI,EAAE;IAEN,MAAM,CAAC,GAAG,MAAM,SAAS,CAAC;QACxB,OAAO;QACP,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ;KACxB,CAAC,CAAC;IACH,YAAY,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,OAAe,EACf,YAAY,GAAG,IAAI;IAEnB,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;IACxD,YAAY,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,EACL,UAAU,IAAI,KAAK,EACnB,UAAU,IAAI,KAAK,EACnB,YAAY,IAAI,OAAO,EACvB,SAAS,IAAI,IAAI,EACjB,GAAG,GACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@captain_z/zsk",
3
+ "version": "0.1.0",
4
+ "description": "ZNorth Standard Kit — CLI installer for zsk skill bundles (npx @captain_z/zsk add)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "zsk": "./dist/bin.js"
9
+ },
10
+ "main": "./dist/bin.js",
11
+ "files": [
12
+ "dist",
13
+ "templates",
14
+ "README.md"
15
+ ],
16
+ "dependencies": {
17
+ "@clack/prompts": "^0.9.0",
18
+ "commander": "^12.1.0",
19
+ "picocolors": "^1.1.1",
20
+ "yaml": "^2.7.0",
21
+ "@captain_z/zsk-skills": "0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.10.0",
25
+ "tsx": "^4.19.2",
26
+ "typescript": "^5.7.3"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p .",
36
+ "dev": "tsx src/bin.ts",
37
+ "typecheck": "tsc -p . --noEmit"
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ ---
2
+ title: Figma Index
3
+ project: <fill-me>
4
+ last_updated: <YYYY-MM-DD>
5
+ ---
6
+
7
+ # Figma Index — <项目名>
8
+
9
+ > **角色**:模块 → Figma URL / node-id 的权威索引。zsk skill 通过 `{{config.paths.figma_index}}` 引用。
10
+ > **配合 skill**:`zsk:ue-mcp`(结构化快照)、`zsk:figma-to-code`(7 步工作流)。
11
+ > **更新时机**:模块首次接入 Figma 设计时登记;设计大改时追加新 snapshot 行。
12
+
13
+ ## 1. 模块索引
14
+
15
+ | 模块 ID | Figma URL | Node ID | 页面 / Frame | 最后确认 | 快照路径 |
16
+ | --- | --- | --- | --- | --- | --- |
17
+ | <module-a> | https://www.figma.com/file/... | <int:int> | <Page / Frame> | <YYYY-MM-DD> | `.raws/design-assets/module-a/<snapshot>/` |
18
+
19
+ ## 2. 工作流
20
+
21
+ 1. 模块接入 Figma 时:登记本表一行
22
+ 2. 走 `zsk:ue-mcp` skill:
23
+ - 创建 `.raws/design-assets/{module}/{YYYY-MM-DD}-{描述}/` 目录
24
+ - 落盘 `description.md`(结构化 YAML frontmatter + 人读章节)
25
+ - 落盘 `screenshot-*.png`(每状态一张)
26
+ - 可选:`raw/mcp-response.json`(`.gitignore`)
27
+ 3. 走 `zsk:figma-to-code`:按 7 步从快照产出生产级代码
28
+
29
+ ## 3. 变更纪律
30
+
31
+ - **新 snapshot**:设计大改 → 新目录,**不覆盖历史**(历史用于回溯)
32
+ - **本表改写**:指向新 snapshot 目录,旧行转入"归档"小节(保留追溯)
33
+ - **node-id 废弃**:Figma 重绘导致 node-id 失效 → 旧行标 `[deprecated]`,新 node-id 新行
34
+
35
+ ## 归档(历史 snapshot)
36
+
37
+ | 模块 ID | 旧 snapshot | 归档原因 | 归档日期 |
38
+ | --- | --- | --- | --- |
@@ -0,0 +1,37 @@
1
+ # .raws/ — 项目原始事实源
2
+
3
+ 本目录是项目的**原始事实源层**,存放由人类或外部系统维护、不由 LLM 自由改写的原始文档。
4
+
5
+ ## 推荐内容
6
+
7
+ | 文件 / 子目录 | 用途 | zsk skill 引用 |
8
+ | --- | --- | --- |
9
+ | `SRS.md` | 原始需求文档(Software Requirements Spec) | `{{config.paths.srs}}` |
10
+ | `FIGMA-INDEX.md` | 模块 → Figma URL / node-id 索引 | `{{config.paths.figma_index}}` |
11
+ | `api-contracts/` | 后端 API 契约(OpenAPI / GraphQL schema) | `{{config.paths.api_contracts}}` |
12
+ | `design-assets/{module}/{snapshot}/` | Figma 快照(由 `zsk:ue-mcp` 产出) | `{{config.paths.design_assets}}` |
13
+
14
+ ## 消费路径
15
+
16
+ skill 通过 `project-config.md` frontmatter 的 `paths.*` 键找到本目录文件:
17
+
18
+ ```
19
+ project-config.md:
20
+ paths:
21
+ srs: .raws/SRS.md
22
+ figma_index: .raws/FIGMA-INDEX.md
23
+ api_contracts: .raws/api-contracts
24
+ design_assets: .raws/design-assets
25
+ ```
26
+
27
+ ## Git 状态建议
28
+
29
+ - `SRS.md` / `FIGMA-INDEX.md`:**入库**(团队共享)
30
+ - `design-assets/{module}/{snapshot}/raw/`:可选 `.gitignore`(体积大或含敏感信息)
31
+ - 含敏感数据的文件:在本目录加 `.gitignore` 排除
32
+
33
+ ## 原则
34
+
35
+ - **源文件不被 LLM 随意改写**:变更走人工/流程
36
+ - **变更即新 snapshot**:Figma 更新→新 snapshot 目录,不覆盖历史
37
+ - **编号锚稳定**:SRS 中的 `FR-001` / `US-001` / `AC-001` 一旦发布不改(仅废弃,合并/拆分留追踪)
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: Software Requirements Specification
3
+ project: <fill-me>
4
+ status: draft
5
+ version: 0.1.0
6
+ last_updated: <YYYY-MM-DD>
7
+ ---
8
+
9
+ # SRS — <项目名>
10
+
11
+ > **角色**:原始需求事实源。zsk skill 通过 `{{config.paths.srs}}` 引用本文件。
12
+ > **变更流程**:走人工评审(见 `zsk:proposal` → `zsk:spec`),不由 LLM 自由改写。
13
+ > **编号纪律**:FR / NFR / AC 只增不改,废弃用 `[deprecated]` 标记。
14
+
15
+ ## 1. 范围与目标
16
+
17
+ ### 1.1 背景
18
+ <为什么要做此产品/模块>
19
+
20
+ ### 1.2 SMART 目标
21
+ <用 zsk:proposal skill 的 G-{n} 格式>
22
+
23
+ ### 1.3 超出范围(Out of Scope)
24
+ <显式列出不做什么,避免范围蔓延>
25
+
26
+ ## 2. 用户角色与核心场景
27
+
28
+ | 角色 | 核心场景 | 优先级 |
29
+ | --- | --- | --- |
30
+ | <角色 A> | <场景描述> | P0 |
31
+
32
+ ## 3. 功能需求(FR)
33
+
34
+ > 编号规则见 `zsk:spec`。每条 FR 必须可被下游 Design / Task / Verify 用编号引用。
35
+
36
+ - **FR-001**: <描述:做什么,不写怎么做>
37
+ - **FR-002**: <...>
38
+
39
+ ## 4. 非功能需求(NFR)
40
+
41
+ > 7 大类:performance / a11y / security / i18n / compat / observability / reliability
42
+ > 项目级基线见 `project-config.md` 的 `quality.*`;模块级偏离单独声明。
43
+
44
+ - **NFR-001**: <描述 + 阈值 + 验证手法>
45
+
46
+ ## 5. 验收条件(AC)
47
+
48
+ > Gherkin(Given/When/Then)或 BDD 可执行化。每条 AC 覆盖至少 1 个 FR。
49
+
50
+ - **AC-001**: Given <前置>, When <操作>, Then <期望结果> · 覆盖 `FR-001`
51
+
52
+ ## 6. 用户故事(US)
53
+
54
+ > INVEST 原则。每个 US 关联若干 FR/AC。
55
+
56
+ - **US-001**: 作为 <角色>,我想 <目标>,以便 <价值> · 覆盖 `FR-001` / `AC-001`
57
+
58
+ ## 7. 约束(Constraints)
59
+
60
+ <业务/合规/技术约束。技术约束尽量走 `project-config.md` 与 ADR,不在此重抄>
61
+
62
+ ## 8. 术语表(Glossary)
63
+
64
+ > 可独立维护于 `docs/system/glossary.md`(zsk:glossary skill)。
65
+
66
+ | 术语 | 定义 |
67
+ | --- | --- |
68
+ | <Term A> | <定义> |
69
+
70
+ ## 9. 开放问题
71
+
72
+ - [ ] <待澄清问题 1>
73
+ - [ ] <待澄清问题 2>