specrails-core 4.11.2 → 4.12.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 (56) hide show
  1. package/README.md +56 -20
  2. package/bin/specrails-core.mjs +343 -20
  3. package/bin/tui-installer.mjs +103 -52
  4. package/dist/installer/cli.js +1 -1
  5. package/dist/installer/cli.js.map +1 -1
  6. package/dist/installer/commands/doctor.js +486 -24
  7. package/dist/installer/commands/doctor.js.map +1 -1
  8. package/dist/installer/commands/framework.js +49 -7
  9. package/dist/installer/commands/framework.js.map +1 -1
  10. package/dist/installer/commands/init.js +423 -25
  11. package/dist/installer/commands/init.js.map +1 -1
  12. package/dist/installer/commands/update.js +36 -9
  13. package/dist/installer/commands/update.js.map +1 -1
  14. package/dist/installer/phases/framework-lifecycle.js +125 -0
  15. package/dist/installer/phases/framework-lifecycle.js.map +1 -0
  16. package/dist/installer/phases/install-config.js +157 -5
  17. package/dist/installer/phases/install-config.js.map +1 -1
  18. package/dist/installer/phases/manifest.js +27 -2
  19. package/dist/installer/phases/manifest.js.map +1 -1
  20. package/dist/installer/phases/prereqs.js +57 -2
  21. package/dist/installer/phases/prereqs.js.map +1 -1
  22. package/dist/installer/phases/provider-detect.js +116 -6
  23. package/dist/installer/phases/provider-detect.js.map +1 -1
  24. package/dist/installer/phases/scaffold.js +1222 -12
  25. package/dist/installer/phases/scaffold.js.map +1 -1
  26. package/dist/installer/runtime/kimi.js +255 -0
  27. package/dist/installer/runtime/kimi.js.map +1 -0
  28. package/dist/installer/util/paths.js +12 -0
  29. package/dist/installer/util/paths.js.map +1 -1
  30. package/dist/installer/util/registry.js +234 -14
  31. package/dist/installer/util/registry.js.map +1 -1
  32. package/docs/README.md +1 -0
  33. package/docs/deployment.md +6 -7
  34. package/docs/getting-started.md +11 -7
  35. package/docs/installation.md +34 -16
  36. package/docs/plugin-architecture.md +11 -8
  37. package/docs/updating.md +21 -3
  38. package/docs/user-docs/cli-reference.md +43 -22
  39. package/docs/user-docs/codex-vs-claude-code.md +11 -9
  40. package/docs/user-docs/faq.md +1 -1
  41. package/docs/user-docs/getting-started-codex.md +5 -8
  42. package/docs/user-docs/getting-started-kimi.md +423 -0
  43. package/docs/user-docs/installation.md +49 -14
  44. package/docs/user-docs/quick-start.md +11 -8
  45. package/docs/windows.md +29 -4
  46. package/integration-contract.json +85 -13
  47. package/package.json +9 -5
  48. package/schemas/profile.v1.json +67 -5
  49. package/templates/codex-skills/batch-implement/SKILL.md +4 -0
  50. package/templates/commands/specrails/batch-implement.md +11 -0
  51. package/templates/gemini-commands/batch-implement.toml +4 -0
  52. package/templates/kimi/specrails/run-skill.mjs +3005 -0
  53. package/templates/kimi/specrails/vendor/js-yaml/LICENSE +21 -0
  54. package/templates/kimi/specrails/vendor/js-yaml/NOTICE.md +16 -0
  55. package/templates/kimi/specrails/vendor/js-yaml/js-yaml.mjs +3856 -0
  56. package/templates/profiles/kimi-default.json +15 -0
@@ -4,6 +4,7 @@ import { InstallerError } from '../util/errors.js';
4
4
  import { ok, step } from '../util/logger.js';
5
5
  import { pathExists, readTextFile } from '../util/fs.js';
6
6
  import { derivedPaths } from '../phases/provider-detect.js';
7
+ import { assertFrameworkVersionComplete, resolveRequiredFrameworkProviders, } from '../phases/framework-lifecycle.js';
7
8
  import { assembleProjectWorkspace, ensureCurrentSymlink } from '../phases/scaffold.js';
8
9
  import { ensureFramework } from './init.js';
9
10
  function requireString(value, flagName) {
@@ -12,13 +13,47 @@ function requireString(value, flagName) {
12
13
  }
13
14
  return value;
14
15
  }
16
+ const SAFE_FRAMEWORK_VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
17
+ /**
18
+ * Framework versions become directory names under the shared framework store.
19
+ * Keep this deliberately narrower than an arbitrary path segment so a version
20
+ * accepted on POSIX remains safe when the same registry/store is consumed on
21
+ * Windows. It accepts normal semver, prerelease, and build identifiers while
22
+ * rejecting separators, dot segments, controls, and traversal payloads.
23
+ */
24
+ function parseFrameworkVersion(value) {
25
+ const version = requireString(value, 'version');
26
+ if (version === '.' ||
27
+ version === '..' ||
28
+ version.includes('\0') ||
29
+ !SAFE_FRAMEWORK_VERSION.test(version)) {
30
+ throw new InstallerError(`--version must be a safe framework version identifier, got: ${JSON.stringify(version)}`, 40);
31
+ }
32
+ return version;
33
+ }
15
34
  function parseProvider(value) {
16
35
  const v = requireString(value, 'provider');
17
- if (v !== 'claude' && v !== 'codex' && v !== 'gemini') {
18
- throw new InstallerError(`--provider value must be 'claude', 'codex', or 'gemini', got: ${v}`, 40);
36
+ if (v !== 'claude' && v !== 'codex' && v !== 'gemini' && v !== 'kimi') {
37
+ throw new InstallerError(`--provider value must be 'claude', 'codex', 'gemini', or 'kimi', got: ${v}`, 40);
19
38
  }
20
39
  return v;
21
40
  }
41
+ function parseProviders(value) {
42
+ if (value === undefined)
43
+ return [];
44
+ const raw = requireString(value, 'providers');
45
+ const providers = [];
46
+ for (const item of raw.split(',')) {
47
+ const provider = item.trim();
48
+ if (provider.length === 0)
49
+ continue;
50
+ providers.push(parseProvider(provider));
51
+ }
52
+ if (providers.length === 0) {
53
+ throw new InstallerError('--providers must contain at least one provider', 40);
54
+ }
55
+ return [...new Set(providers)];
56
+ }
22
57
  /**
23
58
  * Resolves the specrails-core package root (where templates/ + commands/ live).
24
59
  * - Published: `<pkg>/dist/installer/commands/framework.js`
@@ -42,7 +77,7 @@ export async function runInstallFramework(flags) {
42
77
  const scriptDir = resolveScriptDir();
43
78
  const frameworkDir = path.resolve(requireString(flags['framework-dir'], 'framework-dir'));
44
79
  const provider = parseProvider(flags.provider);
45
- const version = requireString(flags.version, 'version');
80
+ const version = parseFrameworkVersion(flags.version);
46
81
  const { providerDir } = derivedPaths(provider);
47
82
  const swapCurrent = flags['no-swap'] !== true;
48
83
  step(`Materializing framework ${version} (${provider}) → ${frameworkDir}`);
@@ -70,11 +105,18 @@ export async function runInstallFramework(flags) {
70
105
  */
71
106
  export async function runSwapCurrent(flags) {
72
107
  const frameworkDir = path.resolve(requireString(flags['framework-dir'], 'framework-dir'));
73
- const version = requireString(flags.version, 'version');
108
+ const version = parseFrameworkVersion(flags.version);
109
+ const requested = parseProviders(flags.providers);
110
+ const required = resolveRequiredFrameworkProviders({
111
+ frameworkDir,
112
+ requested,
113
+ registryHome: process.env.SPECRAILS_REGISTRY_HOME,
114
+ });
74
115
  step(`Swapping framework current → ${version} (${frameworkDir})`);
116
+ const providers = assertFrameworkVersionComplete(frameworkDir, version, required);
75
117
  ensureCurrentSymlink(frameworkDir, version);
76
- ok(`current → ${version}`);
77
- return { frameworkDir, version };
118
+ ok(`current → ${version} (${providers.join(', ')})`);
119
+ return { frameworkDir, version, providers };
78
120
  }
79
121
  /**
80
122
  * `specrails-core assemble` — SYMLINK the materialized framework subtrees into a
@@ -86,7 +128,7 @@ export async function runAssemble(flags) {
86
128
  const workspace = path.resolve(requireString(flags.workspace, 'workspace'));
87
129
  const frameworkDir = path.resolve(requireString(flags['framework-dir'], 'framework-dir'));
88
130
  const provider = parseProvider(flags.provider);
89
- const version = requireString(flags.version, 'version');
131
+ const version = parseFrameworkVersion(flags.version);
90
132
  const codeRoot = path.resolve(requireString(flags['code-root'], 'code-root'));
91
133
  const { providerDir } = derivedPaths(provider);
92
134
  // Defence-in-depth: the framework must be materialized + `current` pointed at
@@ -1 +1 @@
1
- {"version":3,"file":"framework.js","sourceRoot":"","sources":["../../../src/installer/commands/framework.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"framework.js","sourceRoot":"","sources":["../../../src/installer/commands/framework.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,102 @@
1
+ import { copyFileSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, renameSync, rmSync, } from 'node:fs';
2
+ import os from 'node:os';
1
3
  import path from 'node:path';
2
4
  import { fileURLToPath } from 'node:url';
3
5
  import { InstallerError } from '../util/errors.js';
4
6
  import { runCommand } from '../util/exec.js';
5
7
  import { info, ok, step } from '../util/logger.js';
6
- import { readTextFile, pathExists } from '../util/fs.js';
8
+ import { isDir, isSymlink, listDir, pathExists, readTextFile, removePath, writeFileLf, } from '../util/fs.js';
7
9
  import { loadInstallConfig, resolveConfigPath, } from '../phases/install-config.js';
8
10
  import { checkPrerequisites } from '../phases/prereqs.js';
9
11
  import { derivedPaths } from '../phases/provider-detect.js';
10
- import { assembleProjectWorkspace, ensureCurrentSymlink, installFramework, } from '../phases/scaffold.js';
12
+ import { materializeFrameworkVersion } from '../phases/framework-lifecycle.js';
13
+ import { assembleProjectWorkspace, installFramework, } from '../phases/scaffold.js';
11
14
  import { frameworkRoot, resolveArtifacts } from '../util/registry.js';
15
+ const WORKSPACE_PROVIDER_ORDER = [
16
+ 'claude',
17
+ 'codex',
18
+ 'gemini',
19
+ 'kimi',
20
+ ];
21
+ /**
22
+ * Snapshot the provider/role inventory exposed by one live workspace before a
23
+ * global framework version swap. This matters on Windows: directory junctions
24
+ * resolve their target when they are created, so a workspace link made through
25
+ * `framework/current` must be recreated after `current` moves. Keeping the
26
+ * exact visible `sr-*` set also avoids widening or narrowing a project's
27
+ * optional role selection during that refresh.
28
+ */
29
+ export function snapshotWorkspaceProviderSelections(workspace) {
30
+ const selections = {};
31
+ for (const provider of WORKSPACE_PROVIDER_ORDER) {
32
+ const { providerDir } = derivedPaths(provider);
33
+ const providerRoot = path.join(workspace, providerDir);
34
+ if (!pathExists(providerRoot) ||
35
+ !workspaceHasManagedProviderLink(providerRoot, provider)) {
36
+ continue;
37
+ }
38
+ const roleRoot = provider === 'codex'
39
+ ? path.join(workspace, providerDir, 'skills', 'rails')
40
+ : provider === 'kimi'
41
+ ? path.join(workspace, providerDir, 'skills')
42
+ : path.join(workspace, providerDir, 'agents');
43
+ const ids = listDir(roleRoot)
44
+ .filter((entry) => provider === 'claude' || provider === 'gemini'
45
+ ? path.basename(entry).endsWith('.md')
46
+ : isDir(entry))
47
+ .map((entry) => {
48
+ const name = path.basename(entry);
49
+ return name.endsWith('.md') ? name.slice(0, -3) : name;
50
+ })
51
+ .filter((id) => /^sr-[a-z0-9-]+$/.test(id));
52
+ selections[provider] = [...new Set(ids)].sort();
53
+ }
54
+ return selections;
55
+ }
56
+ function workspaceHasManagedProviderLink(providerRoot, provider) {
57
+ const directoryLinks = provider === 'claude'
58
+ ? ['commands', 'skills', 'rules']
59
+ : provider === 'codex'
60
+ ? ['skills']
61
+ : provider === 'gemini'
62
+ ? ['commands']
63
+ : ['rules', 'specrails'];
64
+ if (directoryLinks.some((relative) => isSymlink(path.join(providerRoot, relative)))) {
65
+ return true;
66
+ }
67
+ const granularRoot = provider === 'kimi'
68
+ ? path.join(providerRoot, 'skills')
69
+ : path.join(providerRoot, 'agents');
70
+ return listDir(granularRoot).some((entry) => isSymlink(entry));
71
+ }
72
+ /**
73
+ * Refresh every provider already represented in the project plus the provider
74
+ * selected by the current operation. POSIX symlinks continue to follow
75
+ * `framework/current`; Windows junctions do not reliably preserve that
76
+ * indirection, so rebuilding all live links is required for true
77
+ * multi-provider version parity.
78
+ */
79
+ export function reassembleWorkspaceProviders(input) {
80
+ const providers = WORKSPACE_PROVIDER_ORDER.filter((provider) => provider === input.selectedProvider ||
81
+ Object.prototype.hasOwnProperty.call(input.previousSelections, provider));
82
+ for (const provider of providers) {
83
+ const { providerDir } = derivedPaths(provider);
84
+ assembleProjectWorkspace({
85
+ workspace: input.workspace,
86
+ frameworkDir: input.frameworkDir,
87
+ provider,
88
+ providerDir,
89
+ version: input.version,
90
+ codeRoot: input.codeRoot,
91
+ scriptDir: input.scriptDir,
92
+ selectedAgents: provider === input.selectedProvider
93
+ ? input.selectedAgents ?? input.previousSelections[provider]
94
+ : input.previousSelections[provider],
95
+ copyStatics: input.copyStatics,
96
+ });
97
+ }
98
+ return providers;
99
+ }
12
100
  /**
13
101
  * Entry called by cli.ts. Returns a {@link InitResult} on success;
14
102
  * throws a typed error (translated to an exit code by the outer CLI)
@@ -18,11 +106,25 @@ export async function runInit(flags) {
18
106
  const scriptDir = resolveScriptDir();
19
107
  const version = readVersion(scriptDir);
20
108
  const repoRoot = path.resolve(typeof flags['root-dir'] === 'string' ? flags['root-dir'] : process.cwd());
21
- const autoYes = flags.yes === true;
109
+ const autoYes = flags.yes === true || flags.y === true;
22
110
  const skipPrereqs = process.env.SPECRAILS_SKIP_PREREQS === '1';
111
+ // Validate an explicit provider before consulting --from-config. The TUI
112
+ // intentionally re-enters init with BOTH flags; accepting that flow is safe
113
+ // only when the generated config agrees with the original explicit choice.
114
+ let explicitProvider;
115
+ if (flags.provider !== undefined) {
116
+ if (typeof flags.provider !== 'string' ||
117
+ (flags.provider !== 'claude' &&
118
+ flags.provider !== 'codex' &&
119
+ flags.provider !== 'gemini' &&
120
+ flags.provider !== 'kimi')) {
121
+ throw new InstallerError(`--provider value must be 'claude', 'codex', 'gemini', or 'kimi', got: ${String(flags.provider)}`, 40);
122
+ }
123
+ explicitProvider = flags.provider;
124
+ }
23
125
  // --from-config: read provider + tier from yaml.
24
126
  const fromConfigFlag = flags['from-config'];
25
- let providerHint;
127
+ let providerHint = explicitProvider;
26
128
  let tierHint;
27
129
  let selectedAgentsHint;
28
130
  if (fromConfigFlag !== undefined) {
@@ -30,21 +132,18 @@ export async function runInit(flags) {
30
132
  const resolved = resolveConfigPath(repoRoot, explicitPath);
31
133
  const config = loadInstallConfig(resolved);
32
134
  if (config) {
135
+ if (explicitProvider && explicitProvider !== config.provider) {
136
+ throw new InstallerError(`--provider '${explicitProvider}' conflicts with provider '${config.provider}' in ${resolved}`, 40);
137
+ }
33
138
  providerHint = config.provider;
34
139
  tierHint = config.tier;
35
140
  selectedAgentsHint = config.agents.selected;
36
141
  info(`Loaded install config from ${resolved}`);
37
142
  }
38
143
  else {
39
- info(`install-config.yaml not found at ${resolved} — falling back to auto-detection`);
144
+ info(`install-config.yaml not found at ${resolved} — falling back to ${explicitProvider ? `explicit provider '${explicitProvider}'` : 'auto-detection'}`);
40
145
  }
41
146
  }
42
- else if (typeof flags.provider === 'string') {
43
- if (flags.provider !== 'claude' && flags.provider !== 'codex' && flags.provider !== 'gemini') {
44
- throw new InstallerError(`--provider value must be 'claude', 'codex', or 'gemini', got: ${flags.provider}`, 40);
45
- }
46
- providerHint = flags.provider;
47
- }
48
147
  if (flags.quick === true) {
49
148
  tierHint = 'quick';
50
149
  }
@@ -75,8 +174,21 @@ export async function runInit(flags) {
75
174
  providers: [prereqs.provider],
76
175
  coreVersion: version,
77
176
  });
177
+ // An existing Core-owned relocated entry is also the durable provider
178
+ // inventory. Merge this install's provider without reallocating legacy
179
+ // in-repo installs; Desktop-owned entries remain read-only in the resolver.
180
+ if (artifactRoot !== codeRoot) {
181
+ resolveArtifacts(repoRoot, {
182
+ allocate: true,
183
+ allocator: 'core-standalone',
184
+ home: process.env.SPECRAILS_REGISTRY_HOME,
185
+ providers: [prereqs.provider],
186
+ coreVersion: version,
187
+ });
188
+ }
78
189
  // In-repo when the resolution did NOT relocate the artifacts out of the repo.
79
190
  const inRepo = artifactRoot === codeRoot;
191
+ const previousSelections = snapshotWorkspaceProviderSelections(artifactRoot);
80
192
  // ─── Phase 2 + 3 ──────────────────────────────────────────────────────
81
193
  // Bundled-framework flow: materialize the provider-INVARIANT framework ONCE
82
194
  // under `<home>/.specrails/framework/<version>/<providerDir>/`, point `current`
@@ -94,15 +206,15 @@ export async function runInit(flags) {
94
206
  version,
95
207
  selectedAgents: selectedAgentsHint,
96
208
  });
97
- assembleProjectWorkspace({
209
+ reassembleWorkspaceProviders({
98
210
  workspace: artifactRoot,
99
211
  frameworkDir: fwDir,
100
- provider: prereqs.provider,
101
- providerDir,
102
212
  version,
103
213
  codeRoot,
104
214
  scriptDir,
215
+ selectedProvider: prereqs.provider,
105
216
  selectedAgents: selectedAgentsHint,
217
+ previousSelections,
106
218
  // In-repo: COPY the framework statics as real, committable files. Relocated:
107
219
  // symlink from the framework store (O(1) update on a version swap).
108
220
  copyStatics: inRepo,
@@ -114,11 +226,12 @@ export async function runInit(flags) {
114
226
  ok(`Linked ${providerDir}/ from framework ${version} + seeded project layer`);
115
227
  }
116
228
  // openspec STAYS in the repo (codeRoot) — unchanged behaviour.
117
- await installOpenSpecProject(codeRoot, prereqs.provider);
229
+ await installOpenSpecProject(codeRoot, prereqs.provider, artifactRoot);
118
230
  const tier = tierHint ?? 'full';
119
231
  if (tier === 'full') {
120
232
  step('Next steps');
121
- info('Run `/specrails:enrich` inside Claude Code to complete your setup.');
233
+ const enrichCommand = prereqs.provider === 'kimi' ? '/skill:specrails-enrich' : '/specrails:enrich';
234
+ info(`Run \`${enrichCommand}\` inside your selected provider to complete your setup.`);
122
235
  }
123
236
  else {
124
237
  step('Installation complete');
@@ -148,17 +261,28 @@ export async function runInit(flags) {
148
261
  * ensureCurrentSymlink(frameworkDir, version) // single atomic swap at the end
149
262
  */
150
263
  export function ensureFramework(input) {
151
- installFramework({
264
+ if (input.swapCurrent === false) {
265
+ installFramework({
266
+ scriptDir: input.scriptDir,
267
+ frameworkDir: input.frameworkDir,
268
+ provider: input.provider,
269
+ providerDir: input.providerDir,
270
+ version: input.version,
271
+ selectedAgents: input.selectedAgents,
272
+ });
273
+ return;
274
+ }
275
+ // `current` is global, not scoped to this project/provider. Carry forward the
276
+ // requested provider, every globally registered provider, and every provider
277
+ // represented by the old current version; materialize all without swapping,
278
+ // validate the complete destination, then move the pointer exactly once.
279
+ materializeFrameworkVersion({
152
280
  scriptDir: input.scriptDir,
153
281
  frameworkDir: input.frameworkDir,
154
- provider: input.provider,
155
- providerDir: input.providerDir,
156
282
  version: input.version,
157
- selectedAgents: input.selectedAgents,
283
+ requested: [input.provider, ...(input.requiredProviders ?? [])],
284
+ registryHome: input.registryHome,
158
285
  });
159
- if (input.swapCurrent !== false) {
160
- ensureCurrentSymlink(input.frameworkDir, input.version);
161
- }
162
286
  }
163
287
  /**
164
288
  * Resolves the package root (where VERSION, templates/, commands/ live).
@@ -225,7 +349,9 @@ function readPinnedVersion(scriptDir, key, fallback) {
225
349
  export function buildOpenSpecInvocation(repoRoot, provider, env = process.env, pinnedVersion = readPinnedVersion(resolveScriptDir(), 'openspec', '1.4.1')) {
226
350
  const bin = env.SPECRAILS_OPENSPEC_BIN;
227
351
  const node = env.SPECRAILS_OPENSPEC_NODE;
228
- const initArgs = ['init', '--tools', provider, repoRoot];
352
+ const initArgs = provider === 'kimi'
353
+ ? ['init', '--tools', provider, '--profile', 'custom', repoRoot]
354
+ : ['init', '--tools', provider, repoRoot];
229
355
  if (bin && node) {
230
356
  // Form 1: bundled offline — run the node CLI through the given node exe.
231
357
  return { bin: node, args: [bin, ...initArgs] };
@@ -247,22 +373,294 @@ export function buildOpenSpecInvocation(repoRoot, provider, env = process.env, p
247
373
  ],
248
374
  };
249
375
  }
250
- async function installOpenSpecProject(repoRoot, provider) {
376
+ export const KIMI_REQUIRED_OPENSPEC_SKILLS = [
377
+ 'openspec-propose',
378
+ 'openspec-explore',
379
+ 'openspec-new-change',
380
+ 'openspec-continue-change',
381
+ 'openspec-apply-change',
382
+ 'openspec-ff-change',
383
+ 'openspec-sync-specs',
384
+ 'openspec-archive-change',
385
+ 'openspec-bulk-archive-change',
386
+ 'openspec-verify-change',
387
+ 'openspec-onboard',
388
+ ];
389
+ const OPENSPEC_ALL_WORKFLOWS = [
390
+ 'propose',
391
+ 'explore',
392
+ 'new',
393
+ 'continue',
394
+ 'apply',
395
+ 'ff',
396
+ 'sync',
397
+ 'archive',
398
+ 'bulk-archive',
399
+ 'verify',
400
+ 'onboard',
401
+ ];
402
+ /**
403
+ * Move only OpenSpec-owned Kimi workflow directories from either upstream
404
+ * location into the artifact workspace. Every destination is copied to a
405
+ * sibling temporary directory and renamed, so readers never observe a partial
406
+ * skill. A distinct newly generated source refreshes the managed destination;
407
+ * an in-place corrected destination is retained as-is.
408
+ */
409
+ export function normalizeKimiOpenSpecSkills(repoRoot, artifactRoot) {
410
+ const canonicalRepoRoot = realpathSync(repoRoot);
411
+ const canonicalArtifactRoot = ensureRealDirectoryPath(artifactRoot);
412
+ const destinationRoot = ensureRealDirectoryPath(path.join(canonicalArtifactRoot, '.kimi-code', 'skills'));
413
+ const correctedSourceRoot = path.join(canonicalRepoRoot, '.kimi-code', 'skills');
414
+ const legacySourceRoot = path.join(canonicalRepoRoot, '.kimi', 'skills');
415
+ const correctedIsDestination = path.resolve(correctedSourceRoot) === path.resolve(destinationRoot);
416
+ const installed = [];
417
+ const sources = new Map();
418
+ // Validate the full inventory before mutating either tree.
419
+ for (const skillName of KIMI_REQUIRED_OPENSPEC_SKILLS) {
420
+ const destination = path.join(destinationRoot, skillName);
421
+ const correctedSource = path.join(correctedSourceRoot, skillName);
422
+ const legacySource = path.join(legacySourceRoot, skillName);
423
+ const sourceCandidate = !correctedIsDestination && pathExists(path.join(correctedSource, 'SKILL.md'))
424
+ ? correctedSource
425
+ : pathExists(path.join(legacySource, 'SKILL.md'))
426
+ ? legacySource
427
+ : null;
428
+ const source = sourceCandidate === null
429
+ ? null
430
+ : validateSafeSkillTree(sourceCandidate, skillName);
431
+ sources.set(skillName, source);
432
+ if (source) {
433
+ validateReplaceableDestination(destination, skillName);
434
+ continue;
435
+ }
436
+ if (lstatExists(destination)) {
437
+ validateSafeSkillTree(destination, skillName);
438
+ continue;
439
+ }
440
+ if (pathExists(destination)) {
441
+ throw new InstallerError(`Kimi OpenSpec skill ${skillName} exists without SKILL.md; refusing to overwrite it`, 50);
442
+ }
443
+ throw new InstallerError(`OpenSpec did not generate required Kimi skill ${skillName}; ` +
444
+ 'retry `npx specrails-core update --provider kimi`', 50);
445
+ }
446
+ for (const skillName of KIMI_REQUIRED_OPENSPEC_SKILLS) {
447
+ const destination = path.join(destinationRoot, skillName);
448
+ const source = sources.get(skillName) ?? null;
449
+ if (source) {
450
+ const temporary = mkdtempSync(path.join(destinationRoot, `.${skillName}.specrails-tmp-`));
451
+ const backupContainer = mkdtempSync(path.join(destinationRoot, `.${skillName}.specrails-backup-`));
452
+ const backup = path.join(backupContainer, 'previous');
453
+ copyValidatedSkillTree(source, temporary);
454
+ try {
455
+ validateSafeSkillTree(temporary, skillName);
456
+ }
457
+ catch (error) {
458
+ removePath(temporary);
459
+ removePath(backupContainer);
460
+ throw error;
461
+ }
462
+ const hadDestination = lstatExists(destination);
463
+ try {
464
+ if (hadDestination)
465
+ renameSync(destination, backup);
466
+ renameSync(temporary, destination);
467
+ removePath(backupContainer);
468
+ }
469
+ catch (err) {
470
+ removePath(temporary);
471
+ if (!lstatExists(destination) && lstatExists(backup)) {
472
+ renameSync(backup, destination);
473
+ }
474
+ removePath(backupContainer);
475
+ throw err;
476
+ }
477
+ }
478
+ installed.push(skillName);
479
+ }
480
+ // Remove only the generated, known workflow directories after every
481
+ // destination has been validated. Unknown/user content keeps both trees.
482
+ for (const skillName of KIMI_REQUIRED_OPENSPEC_SKILLS) {
483
+ const legacy = path.join(legacySourceRoot, skillName);
484
+ if (path.resolve(legacy) !== path.resolve(path.join(destinationRoot, skillName))) {
485
+ removePath(legacy);
486
+ }
487
+ if (path.resolve(correctedSourceRoot) !== path.resolve(destinationRoot)) {
488
+ removePath(path.join(correctedSourceRoot, skillName));
489
+ }
490
+ }
491
+ removeDirectoryIfEmpty(legacySourceRoot);
492
+ removeDirectoryIfEmpty(path.dirname(legacySourceRoot));
493
+ if (path.resolve(correctedSourceRoot) !== path.resolve(destinationRoot)) {
494
+ removeDirectoryIfEmpty(correctedSourceRoot);
495
+ removeDirectoryIfEmpty(path.dirname(correctedSourceRoot));
496
+ }
497
+ return installed;
498
+ }
499
+ function lstatExists(target) {
500
+ try {
501
+ lstatSync(target);
502
+ return true;
503
+ }
504
+ catch {
505
+ return false;
506
+ }
507
+ }
508
+ /**
509
+ * Create a directory tree without ever traversing a caller-controlled symlink.
510
+ * Existing ancestor aliases (for example macOS `/var` → `/private/var`) are
511
+ * canonicalized once; every newly appended component must then be a real dir.
512
+ */
513
+ function ensureRealDirectoryPath(directory) {
514
+ const absolute = path.resolve(directory);
515
+ const missing = [];
516
+ let anchor = absolute;
517
+ while (!lstatExists(anchor)) {
518
+ const parent = path.dirname(anchor);
519
+ if (parent === anchor) {
520
+ throw new InstallerError(`cannot resolve directory root for ${absolute}`, 50);
521
+ }
522
+ missing.unshift(path.basename(anchor));
523
+ anchor = parent;
524
+ }
525
+ const anchorMetadata = lstatSync(anchor);
526
+ if (!anchorMetadata.isDirectory() ||
527
+ anchorMetadata.isSymbolicLink()) {
528
+ throw new InstallerError(`Kimi OpenSpec destination must be a real directory: ${absolute}`, 50);
529
+ }
530
+ let cursor = realpathSync(anchor);
531
+ for (const component of missing) {
532
+ cursor = path.join(cursor, component);
533
+ if (!lstatExists(cursor))
534
+ mkdirSync(cursor, { mode: 0o700 });
535
+ const metadata = lstatSync(cursor);
536
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
537
+ throw new InstallerError(`Kimi OpenSpec destination parent must not be a symlink: ${cursor}`, 50);
538
+ }
539
+ }
540
+ return cursor;
541
+ }
542
+ function validateReplaceableDestination(destination, skillName) {
543
+ if (!lstatExists(destination))
544
+ return;
545
+ const metadata = lstatSync(destination);
546
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
547
+ throw new InstallerError(`Kimi OpenSpec destination ${skillName} must be a real directory`, 50);
548
+ }
549
+ }
550
+ function validateSafeSkillTree(source, skillName) {
551
+ let canonicalRoot;
552
+ try {
553
+ const rootMetadata = lstatSync(source);
554
+ if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) {
555
+ throw new Error('skill root is not a real directory');
556
+ }
557
+ canonicalRoot = realpathSync(source);
558
+ }
559
+ catch (error) {
560
+ throw new InstallerError(`unsafe Kimi OpenSpec skill ${skillName}: ${error.message}`, 50);
561
+ }
562
+ const walk = (directory) => {
563
+ for (const name of readdirSync(directory)) {
564
+ const entry = path.join(directory, name);
565
+ const metadata = lstatSync(entry);
566
+ if (metadata.isSymbolicLink()) {
567
+ throw new InstallerError(`unsafe Kimi OpenSpec skill ${skillName}: symlink ${entry}`, 50);
568
+ }
569
+ const relative = path.relative(canonicalRoot, realpathSync(entry));
570
+ if (relative === '..' ||
571
+ relative.startsWith(`..${path.sep}`) ||
572
+ path.isAbsolute(relative)) {
573
+ throw new InstallerError(`unsafe Kimi OpenSpec skill ${skillName}: path escapes source`, 50);
574
+ }
575
+ if (metadata.isDirectory()) {
576
+ walk(entry);
577
+ }
578
+ else if (!metadata.isFile()) {
579
+ throw new InstallerError(`unsafe Kimi OpenSpec skill ${skillName}: unsupported file type ${entry}`, 50);
580
+ }
581
+ }
582
+ };
583
+ walk(canonicalRoot);
584
+ const skillFile = path.join(canonicalRoot, 'SKILL.md');
585
+ if (!lstatExists(skillFile)) {
586
+ throw new InstallerError(`generated Kimi skill ${skillName} is missing SKILL.md`, 50);
587
+ }
588
+ const skillMetadata = lstatSync(skillFile);
589
+ if (!skillMetadata.isFile() || skillMetadata.isSymbolicLink()) {
590
+ throw new InstallerError(`generated Kimi skill ${skillName} has unsafe SKILL.md`, 50);
591
+ }
592
+ return canonicalRoot;
593
+ }
594
+ function copyValidatedSkillTree(source, destination) {
595
+ for (const name of readdirSync(source)) {
596
+ const from = path.join(source, name);
597
+ const to = path.join(destination, name);
598
+ const metadata = lstatSync(from);
599
+ if (metadata.isSymbolicLink()) {
600
+ throw new InstallerError(`refusing to copy Kimi OpenSpec symlink ${from}`, 50);
601
+ }
602
+ if (metadata.isDirectory()) {
603
+ mkdirSync(to, { mode: 0o700 });
604
+ copyValidatedSkillTree(from, to);
605
+ }
606
+ else if (metadata.isFile()) {
607
+ copyFileSync(from, to);
608
+ }
609
+ else {
610
+ throw new InstallerError(`refusing to copy unsupported Kimi OpenSpec file ${from}`, 50);
611
+ }
612
+ }
613
+ }
614
+ function removeDirectoryIfEmpty(dir) {
615
+ if (isDir(dir) && listDir(dir).length === 0)
616
+ removePath(dir);
617
+ }
618
+ export async function installOpenSpecProject(repoRoot, provider, artifactRoot = repoRoot) {
251
619
  if (process.env.SPECRAILS_SKIP_OPENSPEC_INIT === '1') {
620
+ if (provider === 'kimi') {
621
+ const generatedRoots = [
622
+ path.join(repoRoot, '.kimi', 'skills'),
623
+ path.join(repoRoot, '.kimi-code', 'skills'),
624
+ ];
625
+ const hasGeneratedOutput = generatedRoots.some((root) => listDir(root).some((entry) => isDir(entry) && path.basename(entry).startsWith('openspec-')));
626
+ if (hasGeneratedOutput)
627
+ normalizeKimiOpenSpecSkills(repoRoot, artifactRoot);
628
+ }
252
629
  info('Skipping OpenSpec project init (SPECRAILS_SKIP_OPENSPEC_INIT=1)');
253
630
  return;
254
631
  }
255
632
  const { bin, args } = buildOpenSpecInvocation(repoRoot, provider);
633
+ let temporaryConfigHome = null;
634
+ let commandEnv;
635
+ if (provider === 'kimi') {
636
+ temporaryConfigHome = mkdtempSync(path.join(os.tmpdir(), 'specrails-openspec-kimi-'));
637
+ writeFileLf(path.join(temporaryConfigHome, 'openspec', 'config.json'), `${JSON.stringify({
638
+ profile: 'custom',
639
+ delivery: 'skills',
640
+ workflows: OPENSPEC_ALL_WORKFLOWS,
641
+ featureFlags: {},
642
+ }, null, 2)}\n`);
643
+ commandEnv = { ...process.env, XDG_CONFIG_HOME: temporaryConfigHome };
644
+ }
256
645
  step('Phase 3c: Installing OpenSpec');
257
646
  try {
258
647
  await runCommand(bin, args, {
259
648
  cwd: repoRoot,
260
649
  timeoutMs: 180000,
650
+ env: commandEnv,
261
651
  });
652
+ if (provider === 'kimi') {
653
+ normalizeKimiOpenSpecSkills(repoRoot, artifactRoot);
654
+ }
262
655
  ok(`OpenSpec project files installed (${provider})`);
263
656
  }
264
657
  catch (err) {
265
658
  throw new InstallerError(`OpenSpec init failed: ${err.message}`, 50);
266
659
  }
660
+ finally {
661
+ if (temporaryConfigHome) {
662
+ rmSync(temporaryConfigHome, { recursive: true, force: true });
663
+ }
664
+ }
267
665
  }
268
666
  //# sourceMappingURL=init.js.map