@@ -157,6 +157,9 @@ Module owning the per-runtime mapping from artifact kind to filesystem placement
|
||||
### Runtime Artifact Conversion Module
|
||||
Sibling Module to Runtime Artifact Layout Module. Owns projection from canonical Claude-authored command/agent/skill markdown into runtime-specific artifact bodies, including converter selection, frontmatter/body normalization, runtime path rewrites, and staged artifact generation. Runtime Artifact Layout remains responsible for filesystem placement (`kind`, destination subpath, prefix, nesting); Runtime Artifact Conversion owns the content Implementation behind that placement seam so install, uninstall/surface parity, and future plugin/package projections stop reaching back through `bin/install.js` for converter functions or `GSD_TEST_MODE`-guarded installer exports. Chosen direction: sibling Module, not an expanded Layout Module, to preserve ADR-3660's narrow placement responsibility while deepening artifact content locality. First slice: relocate only the layout-reached conversion family (`convertClaudeCommandTo*Skill`, converted command-file emitters, `buildKimiAgentArtifacts`) plus the minimal helper closure they need; do not leave helper dependencies in `bin/install.js` because that would preserve the same shallow seam under a new filename. Installer integration decision: `bin/install.js` imports the conversion Module at top level and re-exports the moved names for compatibility; the conversion Module must not import `bin/install.js` or Runtime Artifact Layout, so the dependency direction becomes installer/layout Adapters -> conversion Module, never conversion -> installer. First-slice Interface decision: export the existing compatibility names only; do not introduce a grouped `convertRuntimeArtifact` Interface until after relocation proves byte-for-byte behavior. SHIPPED (ADR-1508): the converter family relocated in #1510 Phase 1 (`getDirName`→runtime-name-policy, `processAttribution` here); #1511 Phase 2 moved the content-rewrite engine here in full — `_applyRuntimeRewrites` (per-runtime switch, injected attribution), the staged-content walkers `applyRuntimeContentRewritesInPlace`/`applyRuntimeContentRewritesForCommandsInPlace`, `computePathPrefix` (private; `_computePathPrefix` for tests), and the deep public seam `rewriteStagedSkillBodies`/`rewriteStagedCommandBodies({runtime,configDir,scope,homedir?,platform?,resolveAttribution?})`. `bin/install.js` binds these back (single owner, exports preserved); `getCommitAttribution` stays in `bin/install.js` (impure install-time config I/O) and is injected. The `getInstallExports` relay in Runtime Artifact Layout Module was deleted; the dependency direction installer/layout → conversion (never upward) is now enforced. Exception: opencode and kilo path-prefix rewriting is a deliberate `bin/install.js`-owned pre-conversion step (`applyOpencodeFamilyPathPrefix`) per #784, not a violation of the single-owner rule. Source: `gsd-core/bin/lib/runtime-artifact-conversion.cjs` (generated from `src/runtime-artifact-conversion.cts`).
|
||||
|
||||
### Runtime Artifact Install Plan Module
|
||||
Module owning install-time staging and content-rewrite selection for a pre-resolved Runtime Artifact Layout. Interface: `createRuntimeArtifactInstallPlan({ layout, resolvedProfile, homedir?, platform?, resolveAttribution?, deps? }) -> { ok:true, plan:{ items, cleanupDirs } } | { ok:false, kind:'stage_failed'|'rewrite_failed', message, cleanupDirs, failedKind? }`. It iterates `layout.kinds` in order, calls each kind's `stage(resolvedProfile)`, delegates `commands` to Runtime Artifact Conversion `rewriteStagedCommandBodies`, delegates `skills` and `kimi-agents` to `rewriteStagedSkillBodies`, leaves non-rewritten kinds unchanged, and projects copy items as `{ kind, sourceDir, destDir }`. It deliberately does not prune, copy, run legacy migrations, print output, or execute cleanup; those remain Installer Module adapter responsibilities until later slices wire the plan into `bin/install.js`. Source: `gsd-core/bin/lib/runtime-artifact-install-plan.cjs` (generated from `src/runtime-artifact-install-plan.cts`). See Runtime Artifact Layout Module and Runtime Artifact Conversion Module.
|
||||
|
||||
### Command Roster Module
|
||||
Tiny read-only helper Module owning discovery of canonical `commands/gsd/*.md` command stems for artifact conversion and runtime projection. It is a sibling dependency of Runtime Artifact Conversion Module, not part of conversion itself: conversion consumes a roster to safely rewrite `gsd:` / `/gsd-` references, while roster discovery owns filesystem/catalog knowledge. First slice: extract existing `readGsdCommandNames` behavior behind this Module instead of moving it into Runtime Artifact Conversion Module or keeping it as installer-owned state.
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@
|
||||
"roadmap-upgrade.cjs",
|
||||
"roadmap.cjs",
|
||||
"runtime-artifact-conversion.cjs",
|
||||
"runtime-artifact-install-plan.cjs",
|
||||
"runtime-artifact-layout.cjs",
|
||||
"runtime-config-adapter-registry.cjs",
|
||||
"runtime-homes.cjs",
|
||||
|
||||
@@ -476,6 +476,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `roadmap-upgrade.cjs` | Migration tool for converting legacy `Phase N` entries to milestone-prefixed `Phase M-NN` convention; `computeMigrationPlan` + `applyMigration` with dry-run default and atomic rollback |
|
||||
| `roadmap.cjs` | ROADMAP.md parsing, phase extraction, plan progress |
|
||||
| `runtime-artifact-conversion.cjs` | Runtime artifact conversion module — projects Claude-authored commands, agents, and skills into runtime-specific artifact bodies while preserving installer compatibility exports |
|
||||
| `runtime-artifact-install-plan.cjs` | Runtime artifact install plan module — stages pre-resolved layout kinds, applies runtime body rewrites, and returns copy-plan items plus cleanup obligations |
|
||||
| `runtime-artifact-layout.cjs` | Runtime artifact layout module — resolves the artifact directory shapes (commands, agents, skills) for each supported runtime; single source of truth for per-runtime artifact placement (#3663) |
|
||||
| `runtime-config-adapter-registry.cjs` | Explicit runtime config adapter registry — resolves per-runtime config-mutation install intent (install surface, shared-settings gate, finish-phase permission writer); see ADR-58. |
|
||||
| `runtime-hooks-surface.cjs` | Runtime hooks surface module — standalone hook-surface writer functions extracted from bin/install.js (ADR-857 phase 5f-1); owns Cline/Cursor/Copilot/Codex hook artifact generation and reconciliation. |
|
||||
|
||||
@@ -112,6 +112,7 @@ export default tseslint.config(
|
||||
'gsd-core/bin/lib/planning-workspace.cjs',
|
||||
'gsd-core/bin/lib/command-roster.cjs',
|
||||
'gsd-core/bin/lib/runtime-artifact-conversion.cjs',
|
||||
'gsd-core/bin/lib/runtime-artifact-install-plan.cjs',
|
||||
'gsd-core/bin/lib/runtime-artifact-layout.cjs',
|
||||
'gsd-core/bin/lib/runtime-config-adapter-registry.cjs',
|
||||
'gsd-core/bin/lib/runtime-hooks-surface.cjs',
|
||||
|
||||
69
gsd-core/bin/lib/runtime-artifact-install-plan.cjs
Normal file
69
gsd-core/bin/lib/runtime-artifact-install-plan.cjs
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Runtime Artifact Install Plan Module.
|
||||
*
|
||||
* Turns a pre-resolved runtime artifact layout into staged copy inputs. The
|
||||
* installer adapter still owns pruning, copying, migrations, output, and final
|
||||
* cleanup execution.
|
||||
*/
|
||||
// In .cts (CommonJS output) files, `require` is available as a global.
|
||||
const _require = require;
|
||||
const path = _require('node:path');
|
||||
function errorMessage(err) {
|
||||
if (err instanceof Error)
|
||||
return err.message;
|
||||
return String(err);
|
||||
}
|
||||
function addCleanupDir(cleanupDirs, stagedDir, rewrittenDir) {
|
||||
const sourceDir = rewrittenDir ?? stagedDir;
|
||||
if (sourceDir !== stagedDir)
|
||||
cleanupDirs.push(sourceDir);
|
||||
return sourceDir;
|
||||
}
|
||||
function createRuntimeArtifactInstallPlan(args) {
|
||||
const { layout, resolvedProfile, homedir, platform, resolveAttribution, deps = {}, } = args;
|
||||
const conversionExports = _require('./runtime-artifact-conversion.cjs');
|
||||
const rewriteStagedSkillBodies = deps.rewriteStagedSkillBodies ?? conversionExports.rewriteStagedSkillBodies;
|
||||
const rewriteStagedCommandBodies = deps.rewriteStagedCommandBodies ?? conversionExports.rewriteStagedCommandBodies;
|
||||
const cleanupDirs = [];
|
||||
const items = [];
|
||||
const scope = layout.scope ?? 'global';
|
||||
const rewriteOpts = {
|
||||
runtime: layout.runtime,
|
||||
configDir: layout.configDir,
|
||||
scope,
|
||||
homedir,
|
||||
platform,
|
||||
resolveAttribution,
|
||||
};
|
||||
for (const kind of layout.kinds) {
|
||||
let stagedDir;
|
||||
try {
|
||||
stagedDir = kind.stage(resolvedProfile);
|
||||
}
|
||||
catch (err) {
|
||||
return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
||||
}
|
||||
let sourceDir = stagedDir;
|
||||
try {
|
||||
if (kind.kind === 'commands') {
|
||||
const rewrittenDir = rewriteStagedCommandBodies(stagedDir, rewriteOpts);
|
||||
sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
|
||||
}
|
||||
else if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
|
||||
const rewrittenDir = rewriteStagedSkillBodies(stagedDir, rewriteOpts);
|
||||
sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
return { ok: false, kind: 'rewrite_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
||||
}
|
||||
items.push({
|
||||
kind: kind.kind,
|
||||
sourceDir,
|
||||
destDir: path.join(layout.configDir, kind.destSubpath),
|
||||
});
|
||||
}
|
||||
return { ok: true, plan: { items, cleanupDirs } };
|
||||
}
|
||||
module.exports = { createRuntimeArtifactInstallPlan };
|
||||
@@ -120,5 +120,8 @@
|
||||
"test:coverage:all": "npm run test:coverage",
|
||||
"test:mutation": "stryker run",
|
||||
"test:mutation:since": "stryker run --incremental --since origin/next"
|
||||
},
|
||||
"allowScripts": {
|
||||
"fallow@2.70.0": true
|
||||
}
|
||||
}
|
||||
|
||||
146
src/runtime-artifact-install-plan.cts
Normal file
146
src/runtime-artifact-install-plan.cts
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Runtime Artifact Install Plan Module.
|
||||
*
|
||||
* Turns a pre-resolved runtime artifact layout into staged copy inputs. The
|
||||
* installer adapter still owns pruning, copying, migrations, output, and final
|
||||
* cleanup execution.
|
||||
*/
|
||||
|
||||
// In .cts (CommonJS output) files, `require` is available as a global.
|
||||
const _require: NodeRequire = require;
|
||||
const path = _require('node:path') as typeof import('node:path');
|
||||
|
||||
type ArtifactKindName = 'commands' | 'agents' | 'skills' | 'kimi-agents';
|
||||
type InstallScope = 'local' | 'global';
|
||||
|
||||
interface ResolvedProfile {
|
||||
name?: string;
|
||||
skills?: Set<string> | '*';
|
||||
agents?: Set<string>;
|
||||
}
|
||||
|
||||
interface ArtifactKind {
|
||||
kind: ArtifactKindName;
|
||||
destSubpath: string;
|
||||
stage: (resolvedProfile: ResolvedProfile) => string;
|
||||
}
|
||||
|
||||
interface Layout {
|
||||
runtime: string;
|
||||
configDir: string;
|
||||
scope?: InstallScope;
|
||||
kinds: ArtifactKind[];
|
||||
}
|
||||
|
||||
interface RewriteOpts {
|
||||
runtime: string;
|
||||
configDir: string;
|
||||
scope: InstallScope;
|
||||
homedir?: () => string;
|
||||
platform?: NodeJS.Platform;
|
||||
resolveAttribution?: (runtime: string) => string | null | undefined;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
rewriteStagedSkillBodies?: (stagedDir: string, opts: RewriteOpts) => string | void;
|
||||
rewriteStagedCommandBodies?: (stagedDir: string, opts: RewriteOpts) => string | void;
|
||||
}
|
||||
|
||||
interface RuntimeArtifactConversionExports {
|
||||
rewriteStagedSkillBodies: (stagedDir: string, opts: RewriteOpts) => string | void;
|
||||
rewriteStagedCommandBodies: (stagedDir: string, opts: RewriteOpts) => string | void;
|
||||
}
|
||||
|
||||
interface PlanItem {
|
||||
kind: ArtifactKindName;
|
||||
sourceDir: string;
|
||||
destDir: string;
|
||||
}
|
||||
|
||||
interface InstallPlan {
|
||||
items: PlanItem[];
|
||||
cleanupDirs: string[];
|
||||
}
|
||||
|
||||
type InstallPlanResult =
|
||||
| { ok: true; plan: InstallPlan }
|
||||
| { ok: false; kind: 'stage_failed' | 'rewrite_failed'; message: string; cleanupDirs: string[]; failedKind?: ArtifactKindName };
|
||||
|
||||
interface CreateRuntimeArtifactInstallPlanArgs {
|
||||
layout: Layout;
|
||||
resolvedProfile: ResolvedProfile;
|
||||
homedir?: () => string;
|
||||
platform?: NodeJS.Platform;
|
||||
resolveAttribution?: (runtime: string) => string | null | undefined;
|
||||
deps?: Dependencies;
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
if (err instanceof Error) return err.message;
|
||||
return String(err);
|
||||
}
|
||||
|
||||
function addCleanupDir(cleanupDirs: string[], stagedDir: string, rewrittenDir: string | void): string {
|
||||
const sourceDir = rewrittenDir ?? stagedDir;
|
||||
if (sourceDir !== stagedDir) cleanupDirs.push(sourceDir);
|
||||
return sourceDir;
|
||||
}
|
||||
|
||||
function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlanArgs): InstallPlanResult {
|
||||
const {
|
||||
layout,
|
||||
resolvedProfile,
|
||||
homedir,
|
||||
platform,
|
||||
resolveAttribution,
|
||||
deps = {},
|
||||
} = args;
|
||||
const conversionExports = _require('./runtime-artifact-conversion.cjs') as RuntimeArtifactConversionExports;
|
||||
const rewriteStagedSkillBodies = deps.rewriteStagedSkillBodies ?? conversionExports.rewriteStagedSkillBodies;
|
||||
const rewriteStagedCommandBodies = deps.rewriteStagedCommandBodies ?? conversionExports.rewriteStagedCommandBodies;
|
||||
const cleanupDirs: string[] = [];
|
||||
const items: PlanItem[] = [];
|
||||
const scope = layout.scope ?? 'global';
|
||||
const rewriteOpts: RewriteOpts = {
|
||||
runtime: layout.runtime,
|
||||
configDir: layout.configDir,
|
||||
scope,
|
||||
homedir,
|
||||
platform,
|
||||
resolveAttribution,
|
||||
};
|
||||
|
||||
for (const kind of layout.kinds) {
|
||||
let stagedDir: string;
|
||||
try {
|
||||
stagedDir = kind.stage(resolvedProfile);
|
||||
} catch (err) {
|
||||
return { ok: false, kind: 'stage_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
||||
}
|
||||
|
||||
let sourceDir = stagedDir;
|
||||
try {
|
||||
if (kind.kind === 'commands') {
|
||||
const rewrittenDir = rewriteStagedCommandBodies(stagedDir, rewriteOpts);
|
||||
sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
|
||||
} else if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
|
||||
const rewrittenDir = rewriteStagedSkillBodies(stagedDir, rewriteOpts);
|
||||
sourceDir = addCleanupDir(cleanupDirs, stagedDir, rewrittenDir);
|
||||
}
|
||||
} catch (err) {
|
||||
return { ok: false, kind: 'rewrite_failed', message: errorMessage(err), cleanupDirs, failedKind: kind.kind };
|
||||
}
|
||||
|
||||
items.push({
|
||||
kind: kind.kind,
|
||||
sourceDir,
|
||||
destDir: path.join(layout.configDir, kind.destSubpath),
|
||||
});
|
||||
}
|
||||
|
||||
return { ok: true, plan: { items, cleanupDirs } };
|
||||
}
|
||||
|
||||
export = { createRuntimeArtifactInstallPlan };
|
||||
171
tests/runtime-artifact-install-plan.test.cjs
Normal file
171
tests/runtime-artifact-install-plan.test.cjs
Normal file
@@ -0,0 +1,171 @@
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createRuntimeArtifactInstallPlan } = require('../gsd-core/bin/lib/runtime-artifact-install-plan.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
function kind(name, destSubpath, stagedDir, calls) {
|
||||
return {
|
||||
kind: name,
|
||||
destSubpath,
|
||||
prefix: 'gsd-',
|
||||
stage: (resolvedProfile) => {
|
||||
calls.push([name, resolvedProfile.name]);
|
||||
return stagedDir;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('createRuntimeArtifactInstallPlan', () => {
|
||||
test('stages layout kinds in order and projects rewritten source dirs', () => {
|
||||
const configDir = path.join(os.tmpdir(), 'gsd-plan-config');
|
||||
const calls = [];
|
||||
const rewriteCalls = [];
|
||||
const layout = {
|
||||
runtime: 'claude',
|
||||
configDir,
|
||||
scope: 'global',
|
||||
kinds: [
|
||||
kind('commands', 'commands', '/tmp/staged-commands', calls),
|
||||
kind('agents', 'agents', '/tmp/staged-agents', calls),
|
||||
kind('skills', 'skills', '/tmp/staged-skills', calls),
|
||||
kind('kimi-agents', 'agents', '/tmp/staged-kimi-agents', calls),
|
||||
],
|
||||
};
|
||||
|
||||
const result = createRuntimeArtifactInstallPlan({
|
||||
layout,
|
||||
resolvedProfile: { name: 'core' },
|
||||
deps: {
|
||||
rewriteStagedSkillBodies: (stagedDir, opts) => {
|
||||
rewriteCalls.push(['skills', stagedDir, opts.runtime, opts.configDir, opts.scope]);
|
||||
return stagedDir;
|
||||
},
|
||||
rewriteStagedCommandBodies: (stagedDir, opts) => {
|
||||
rewriteCalls.push(['commands', stagedDir, opts.runtime, opts.configDir, opts.scope]);
|
||||
return `${stagedDir}-rewritten`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(calls, [
|
||||
['commands', 'core'],
|
||||
['agents', 'core'],
|
||||
['skills', 'core'],
|
||||
['kimi-agents', 'core'],
|
||||
]);
|
||||
assert.deepStrictEqual(rewriteCalls, [
|
||||
['commands', '/tmp/staged-commands', 'claude', configDir, 'global'],
|
||||
['skills', '/tmp/staged-skills', 'claude', configDir, 'global'],
|
||||
['skills', '/tmp/staged-kimi-agents', 'claude', configDir, 'global'],
|
||||
]);
|
||||
assert.deepStrictEqual(result, {
|
||||
ok: true,
|
||||
plan: {
|
||||
cleanupDirs: ['/tmp/staged-commands-rewritten'],
|
||||
items: [
|
||||
{ kind: 'commands', sourceDir: '/tmp/staged-commands-rewritten', destDir: path.join(configDir, 'commands') },
|
||||
{ kind: 'agents', sourceDir: '/tmp/staged-agents', destDir: path.join(configDir, 'agents') },
|
||||
{ kind: 'skills', sourceDir: '/tmp/staged-skills', destDir: path.join(configDir, 'skills') },
|
||||
{ kind: 'kimi-agents', sourceDir: '/tmp/staged-kimi-agents', destDir: path.join(configDir, 'agents') },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns stage_failed when a layout kind stage adapter throws', () => {
|
||||
const configDir = path.join(os.tmpdir(), 'gsd-plan-config');
|
||||
const layout = {
|
||||
runtime: 'claude',
|
||||
configDir,
|
||||
scope: 'global',
|
||||
kinds: [
|
||||
{
|
||||
kind: 'skills',
|
||||
destSubpath: 'skills',
|
||||
stage: () => { throw new Error('stage boom'); },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createRuntimeArtifactInstallPlan({
|
||||
layout,
|
||||
resolvedProfile: { name: 'core' },
|
||||
deps: {
|
||||
rewriteStagedSkillBodies: () => { throw new Error('must not rewrite after stage failure'); },
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.kind, 'stage_failed');
|
||||
assert.strictEqual(result.failedKind, 'skills');
|
||||
assert.strictEqual(result.message, 'stage boom');
|
||||
assert.deepStrictEqual(result.cleanupDirs, []);
|
||||
});
|
||||
|
||||
test('returns rewrite_failed with prior cleanup obligations when conversion throws', () => {
|
||||
const configDir = path.join(os.tmpdir(), 'gsd-plan-config');
|
||||
const calls = [];
|
||||
const layout = {
|
||||
runtime: 'claude',
|
||||
configDir,
|
||||
scope: 'global',
|
||||
kinds: [
|
||||
kind('commands', 'commands', '/tmp/staged-commands', calls),
|
||||
kind('skills', 'skills', '/tmp/staged-skills', calls),
|
||||
],
|
||||
};
|
||||
|
||||
const result = createRuntimeArtifactInstallPlan({
|
||||
layout,
|
||||
resolvedProfile: { name: 'core' },
|
||||
deps: {
|
||||
rewriteStagedCommandBodies: (stagedDir) => `${stagedDir}-rewritten`,
|
||||
rewriteStagedSkillBodies: () => { throw new Error('rewrite boom'); },
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.kind, 'rewrite_failed');
|
||||
assert.strictEqual(result.failedKind, 'skills');
|
||||
assert.strictEqual(result.message, 'rewrite boom');
|
||||
assert.deepStrictEqual(result.cleanupDirs, ['/tmp/staged-commands-rewritten']);
|
||||
});
|
||||
|
||||
test('uses real command rewrite seam by default', (t) => {
|
||||
const stagedCommands = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-install-plan-commands-'));
|
||||
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-install-plan-config-'));
|
||||
t.after(() => {
|
||||
cleanup(stagedCommands);
|
||||
cleanup(configDir);
|
||||
});
|
||||
fs.writeFileSync(path.join(stagedCommands, 'help.md'), '# help\n');
|
||||
const layout = {
|
||||
runtime: 'claude',
|
||||
configDir,
|
||||
scope: 'global',
|
||||
kinds: [kind('commands', 'commands', stagedCommands, [])],
|
||||
};
|
||||
|
||||
const result = createRuntimeArtifactInstallPlan({
|
||||
layout,
|
||||
resolvedProfile: { name: 'core' },
|
||||
resolveAttribution: () => undefined,
|
||||
homedir: () => '/Users/example',
|
||||
platform: 'linux',
|
||||
});
|
||||
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(result.plan.items.length, 1);
|
||||
assert.strictEqual(result.plan.items[0].kind, 'commands');
|
||||
assert.notStrictEqual(result.plan.items[0].sourceDir, stagedCommands);
|
||||
assert.ok(fs.existsSync(path.join(result.plan.items[0].sourceDir, 'help.md')));
|
||||
assert.deepStrictEqual(result.plan.cleanupDirs, [result.plan.items[0].sourceDir]);
|
||||
for (const dir of result.plan.cleanupDirs) cleanup(dir);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user