scriveno 2.7.1 → 2.8.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/bin/install.js CHANGED
@@ -218,9 +218,167 @@ const RUNTIMES = {
218
218
  },
219
219
  };
220
220
 
221
- function generateSkillManifest(constraintsPath) {
221
+ const SURFACE_PROFILES = {
222
+ core: {
223
+ label: 'Core',
224
+ description: 'Main writing loop and orientation commands.',
225
+ commands: [
226
+ 'new-work',
227
+ 'profile-writer',
228
+ 'voice-test',
229
+ 'discuss',
230
+ 'plan',
231
+ 'draft',
232
+ 'editor-review',
233
+ 'submit',
234
+ 'progress',
235
+ 'save',
236
+ 'next',
237
+ 'health',
238
+ 'help',
239
+ 'surface',
240
+ 'proof-unit',
241
+ ],
242
+ },
243
+ writing: {
244
+ label: 'Writing',
245
+ description: 'Core workflow plus revision, structure, character, and quality commands.',
246
+ includeProfiles: ['core'],
247
+ categories: ['structure', 'structure_management', 'character_world', 'quality', 'review', 'session', 'navigation'],
248
+ },
249
+ publishing: {
250
+ label: 'Publishing',
251
+ description: 'Core workflow plus export, publishing, metadata, and platform packaging commands.',
252
+ includeProfiles: ['core'],
253
+ categories: ['publishing'],
254
+ },
255
+ translation: {
256
+ label: 'Translation',
257
+ description: 'Core workflow plus translation, localization, glossary, and multi-publish commands.',
258
+ includeProfiles: ['core'],
259
+ categories: ['translation'],
260
+ },
261
+ specialist: {
262
+ label: 'Specialist',
263
+ description: 'Core workflow plus sacred, illustration, collaboration, and utility surfaces.',
264
+ includeProfiles: ['core'],
265
+ categories: ['sacred_exclusive', 'illustration', 'collaboration', 'utility'],
266
+ },
267
+ full: {
268
+ label: 'Full',
269
+ description: 'Every Scriveno command.',
270
+ all: true,
271
+ },
272
+ };
273
+
274
+ const DEFAULT_SURFACE_PROFILE = 'full';
275
+
276
+ function normalizeSurfaceProfile(profile) {
277
+ const value = String(profile || DEFAULT_SURFACE_PROFILE).trim().toLowerCase();
278
+ if (!Object.prototype.hasOwnProperty.call(SURFACE_PROFILES, value)) {
279
+ throw new Error(`Unknown profile "${profile}". Expected one of: ${Object.keys(SURFACE_PROFILES).join(', ')}`);
280
+ }
281
+ return value;
282
+ }
283
+
284
+ function resolveProfileCommandKeys(profile, constraints = loadConstraintsForInstall()) {
285
+ const resolvedProfile = normalizeSurfaceProfile(profile);
286
+ const commands = constraints.commands || {};
287
+ const out = new Set();
288
+ const visiting = new Set();
289
+
290
+ function addProfile(name) {
291
+ const key = normalizeSurfaceProfile(name);
292
+ if (visiting.has(key)) {
293
+ throw new Error(`Profile cycle detected at "${key}"`);
294
+ }
295
+ visiting.add(key);
296
+ const spec = SURFACE_PROFILES[key];
297
+ if (spec.all) {
298
+ for (const commandKey of Object.keys(commands)) out.add(commandKey);
299
+ visiting.delete(key);
300
+ return;
301
+ }
302
+ for (const parent of spec.includeProfiles || []) addProfile(parent);
303
+ for (const commandKey of spec.commands || []) {
304
+ if (Object.prototype.hasOwnProperty.call(commands, commandKey)) out.add(commandKey);
305
+ }
306
+ const categorySet = new Set(spec.categories || []);
307
+ if (categorySet.size > 0) {
308
+ for (const [commandKey, command] of Object.entries(commands)) {
309
+ if (categorySet.has(command.category)) out.add(commandKey);
310
+ }
311
+ }
312
+ visiting.delete(key);
313
+ }
314
+
315
+ addProfile(resolvedProfile);
316
+ return out;
317
+ }
318
+
319
+ function loadConstraintsForInstall(constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) {
320
+ return JSON.parse(fs.readFileSync(constraintsPath, 'utf8'));
321
+ }
322
+
323
+ function commandEntryInProfile(entry, commandKeys) {
324
+ return commandKeys.has(commandRefToConstraintKey(entry.commandRef));
325
+ }
326
+
327
+ function collectCommandEntriesForProfile(commandsRoot, profile = DEFAULT_SURFACE_PROFILE, constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) {
328
+ const entries = collectCommandEntries(commandsRoot);
329
+ const commandKeys = resolveProfileCommandKeys(profile, loadConstraintsForInstall(constraintsPath));
330
+ return entries.filter((entry) => commandEntryInProfile(entry, commandKeys));
331
+ }
332
+
333
+ function collectInstallCommandEntries(profile = DEFAULT_SURFACE_PROFILE) {
334
+ return collectCommandEntriesForProfile(
335
+ path.join(PKG_ROOT, 'commands', 'scr'),
336
+ profile,
337
+ path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')
338
+ );
339
+ }
340
+
341
+ function writeProfileCommandFiles(commandsRoot, commandsDir, commandEntries, transform = null) {
342
+ let count = 0;
343
+ for (const entry of commandEntries) {
344
+ const sourcePath = path.join(commandsRoot, entry.relativePath);
345
+ const sourceContent = fs.readFileSync(sourcePath, 'utf8');
346
+ const targetPath = path.join(commandsDir, entry.relativePath);
347
+ atomicWriteFileSync(targetPath, transform ? transform(entry, sourceContent) : sourceContent);
348
+ count++;
349
+ }
350
+ return count;
351
+ }
352
+
353
+ function writeSurfaceProfileMarker(targetDir, profile) {
354
+ if (!targetDir) return;
355
+ fs.mkdirSync(targetDir, { recursive: true });
356
+ atomicWriteFileSync(path.join(targetDir, '.scriveno-profile'), `${normalizeSurfaceProfile(profile)}\n`);
357
+ }
358
+
359
+ function surfaceProfileSummary(profile = DEFAULT_SURFACE_PROFILE, constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) {
360
+ const resolvedProfile = normalizeSurfaceProfile(profile);
361
+ const constraints = loadConstraintsForInstall(constraintsPath);
362
+ const commandKeys = resolveProfileCommandKeys(resolvedProfile, constraints);
363
+ return {
364
+ profile: resolvedProfile,
365
+ label: SURFACE_PROFILES[resolvedProfile].label,
366
+ description: SURFACE_PROFILES[resolvedProfile].description,
367
+ commandCount: commandKeys.size,
368
+ commands: [...commandKeys].sort(),
369
+ };
370
+ }
371
+
372
+ function listSurfaceProfiles(constraintsPath = path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json')) {
373
+ return Object.keys(SURFACE_PROFILES).map((profile) => surfaceProfileSummary(profile, constraintsPath));
374
+ }
375
+
376
+ function generateSkillManifest(constraintsPath, profile = DEFAULT_SURFACE_PROFILE) {
222
377
  const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr');
223
- const entries = collectCanonicalCommandInventory(commandsRoot, constraintsPath).map((entry) => ({
378
+ const profileKeys = resolveProfileCommandKeys(profile, loadConstraintsForInstall(constraintsPath));
379
+ const entries = collectCanonicalCommandInventory(commandsRoot, constraintsPath)
380
+ .filter((entry) => commandEntryInProfile(entry, profileKeys))
381
+ .map((entry) => ({
224
382
  name: entry.commandRef,
225
383
  category: entry.category,
226
384
  description: entry.description,
@@ -239,6 +397,7 @@ function generateSkillManifest(constraintsPath) {
239
397
  return `# Scriveno -- AI Creative Writing Skills
240
398
 
241
399
  Version: ${VERSION}
400
+ Profile: ${normalizeSurfaceProfile(profile)}
242
401
 
243
402
  Scriveno is a spec-driven creative writing, publishing, and translation pipeline.
244
403
 
@@ -829,12 +988,18 @@ function printHelp() {
829
988
  scriveno smoke --json
830
989
  scriveno agents --json
831
990
  scriveno routes --json
991
+ scriveno surface list
992
+ scriveno surface status
993
+ scriveno surface profile core --runtimes codex --project
832
994
  scriveno --runtimes codex,claude-code --global --writer --silent
833
995
 
834
996
  Options:
835
997
  --runtimes <list> Comma-separated runtime keys to install (for example: codex,claude-code)
836
998
  --runtime <key> Repeatable single-runtime selector
837
999
  --detected Install to every detected runtime
1000
+ --profile <name> Command profile: ${Object.keys(SURFACE_PROFILES).join(', ')}
1001
+ --dry-run Show planned writes without changing files
1002
+ --json Print machine-readable output for supported commands
838
1003
  --global Install for all projects (default)
839
1004
  --project Install only in the current directory
840
1005
  --writer Use writer mode (default)
@@ -856,6 +1021,7 @@ Audit commands:
856
1021
  smoke Smoke-test installed runtime surfaces
857
1022
  agents Inspect installed agent prompts and metadata
858
1023
  routes Audit route graph and automation lanes
1024
+ surface Inspect or change the installed command profile
859
1025
 
860
1026
  Runtime keys:
861
1027
  ${Object.keys(RUNTIMES).join(', ')}
@@ -878,6 +1044,11 @@ function parseArgs(argv) {
878
1044
  statusApplySafe: false,
879
1045
  auditJson: false,
880
1046
  syncCheck: false,
1047
+ installProfile: DEFAULT_SURFACE_PROFILE,
1048
+ installDryRun: false,
1049
+ installJson: false,
1050
+ surfaceAction: 'status',
1051
+ surfaceProfile: DEFAULT_SURFACE_PROFILE,
881
1052
  };
882
1053
 
883
1054
  if (argv[0] === 'status' || argv[0] === 'first-run') {
@@ -959,6 +1130,72 @@ function parseArgs(argv) {
959
1130
  }
960
1131
  }
961
1132
 
1133
+ if (argv[0] === 'surface') {
1134
+ options.command = 'surface';
1135
+ let actionSet = false;
1136
+ for (let i = 1; i < argv.length; i++) {
1137
+ const arg = argv[i];
1138
+ if (arg === '--help' || arg === '-h') {
1139
+ options.showHelp = true;
1140
+ } else if (arg === '--version' || arg === '-v') {
1141
+ options.showVersion = true;
1142
+ } else if (arg === '--json') {
1143
+ options.installJson = true;
1144
+ } else if (arg === '--silent' || arg === '--yes') {
1145
+ options.silent = true;
1146
+ } else if (arg === '--dry-run') {
1147
+ options.installDryRun = true;
1148
+ } else if (arg === '--detected') {
1149
+ options.installDetected = true;
1150
+ } else if (arg === '--global') {
1151
+ options.isGlobal = true;
1152
+ } else if (arg === '--project') {
1153
+ options.isGlobal = false;
1154
+ } else if (arg === '--writer') {
1155
+ options.developerMode = false;
1156
+ } else if (arg === '--developer') {
1157
+ options.developerMode = true;
1158
+ } else if (arg === '--runtime') {
1159
+ const value = argv[i + 1];
1160
+ if (!value) throw new Error('--runtime requires a value');
1161
+ addRuntimeList(value);
1162
+ i++;
1163
+ } else if (arg.startsWith('--runtime=')) {
1164
+ addRuntimeList(arg.slice('--runtime='.length));
1165
+ } else if (arg === '--runtimes') {
1166
+ const value = argv[i + 1];
1167
+ if (!value) throw new Error('--runtimes requires a value');
1168
+ addRuntimeList(value);
1169
+ i++;
1170
+ } else if (arg.startsWith('--runtimes=')) {
1171
+ addRuntimeList(arg.slice('--runtimes='.length));
1172
+ } else if (arg === '--profile') {
1173
+ const value = argv[i + 1];
1174
+ if (!value) throw new Error('--profile requires a value');
1175
+ options.surfaceProfile = normalizeSurfaceProfile(value);
1176
+ options.installProfile = options.surfaceProfile;
1177
+ i++;
1178
+ } else if (arg.startsWith('--profile=')) {
1179
+ options.surfaceProfile = normalizeSurfaceProfile(arg.slice('--profile='.length));
1180
+ options.installProfile = options.surfaceProfile;
1181
+ } else if (arg.startsWith('-')) {
1182
+ throw new Error(`Unknown surface argument "${arg}"`);
1183
+ } else if (!actionSet) {
1184
+ options.surfaceAction = arg;
1185
+ actionSet = true;
1186
+ } else if (options.surfaceAction === 'profile') {
1187
+ options.surfaceProfile = normalizeSurfaceProfile(arg);
1188
+ options.installProfile = options.surfaceProfile;
1189
+ } else {
1190
+ throw new Error(`Unknown surface argument "${arg}"`);
1191
+ }
1192
+ }
1193
+ if (!['status', 'list', 'profile'].includes(options.surfaceAction)) {
1194
+ throw new Error(`Unknown surface action "${options.surfaceAction}". Expected status, list, or profile.`);
1195
+ }
1196
+ return options;
1197
+ }
1198
+
962
1199
  for (let i = 0; i < argv.length; i++) {
963
1200
  const arg = argv[i];
964
1201
  if (arg === '--help' || arg === '-h') {
@@ -967,6 +1204,10 @@ function parseArgs(argv) {
967
1204
  options.showVersion = true;
968
1205
  } else if (arg === '--silent' || arg === '--yes') {
969
1206
  options.silent = true;
1207
+ } else if (arg === '--dry-run') {
1208
+ options.installDryRun = true;
1209
+ } else if (arg === '--json') {
1210
+ options.installJson = true;
970
1211
  } else if (arg === '--detected') {
971
1212
  options.installDetected = true;
972
1213
  } else if (arg === '--global') {
@@ -991,6 +1232,13 @@ function parseArgs(argv) {
991
1232
  i++;
992
1233
  } else if (arg.startsWith('--runtimes=')) {
993
1234
  addRuntimeList(arg.slice('--runtimes='.length));
1235
+ } else if (arg === '--profile') {
1236
+ const value = argv[i + 1];
1237
+ if (!value) throw new Error('--profile requires a value');
1238
+ options.installProfile = normalizeSurfaceProfile(value);
1239
+ i++;
1240
+ } else if (arg.startsWith('--profile=')) {
1241
+ options.installProfile = normalizeSurfaceProfile(arg.slice('--profile='.length));
994
1242
  } else {
995
1243
  throw new Error(`Unknown argument "${arg}"`);
996
1244
  }
@@ -1144,6 +1392,98 @@ function runRouteAudit({ json }) {
1144
1392
  return result;
1145
1393
  }
1146
1394
 
1395
+ function resolveSurfaceDataDir(isGlobal) {
1396
+ if (isGlobal === false) return path.resolve('.scriveno');
1397
+ if (isGlobal === true) return path.join(os.homedir(), '.scriveno');
1398
+ const projectDir = path.resolve('.scriveno');
1399
+ const projectSettings = path.join(projectDir, 'settings.json');
1400
+ return fs.existsSync(projectSettings) ? projectDir : path.join(os.homedir(), '.scriveno');
1401
+ }
1402
+
1403
+ function readSurfaceSettings(isGlobal) {
1404
+ const dataDir = resolveSurfaceDataDir(isGlobal);
1405
+ const settingsPath = path.join(dataDir, 'settings.json');
1406
+ const raw = readJsonIfExists(settingsPath);
1407
+ if (!raw) {
1408
+ return { dataDir, settings: null, settingsPath };
1409
+ }
1410
+ try {
1411
+ const settings = migrateSettings(raw);
1412
+ const validation = validateSettings(settings);
1413
+ return { dataDir, settings, settingsPath, validation };
1414
+ } catch (err) {
1415
+ return { dataDir, settings: null, settingsPath, error: err.message };
1416
+ }
1417
+ }
1418
+
1419
+ function formatSurfaceList(profiles) {
1420
+ return [
1421
+ 'Scriveno command profiles',
1422
+ ...profiles.map((profile) => `- ${profile.profile}: ${profile.commandCount} registered commands, ${profile.description}`),
1423
+ ].join('\n');
1424
+ }
1425
+
1426
+ function formatSurfaceStatus(status) {
1427
+ const lines = [
1428
+ 'Scriveno surface status',
1429
+ `Settings: ${status.settingsPath}`,
1430
+ ];
1431
+ if (!status.settings) {
1432
+ lines.push('Installed profile: not found');
1433
+ } else {
1434
+ lines.push(`Installed profile: ${status.settings.profile || DEFAULT_SURFACE_PROFILE}`);
1435
+ lines.push(`Installed runtimes: ${(status.settings.runtimes || []).join(', ') || 'none recorded'}`);
1436
+ lines.push(`Scope: ${status.settings.scope || 'unknown'}`);
1437
+ lines.push(`Mode: ${status.settings.developer_mode ? 'developer' : 'writer'}`);
1438
+ }
1439
+ lines.push('');
1440
+ lines.push('Available profiles:');
1441
+ for (const profile of listSurfaceProfiles()) {
1442
+ lines.push(`- ${profile.profile}: ${profile.commandCount} registered commands`);
1443
+ }
1444
+ lines.push('');
1445
+ lines.push('Use `scriveno surface profile <name> --runtimes <runtime>` to reinstall a smaller or larger surface.');
1446
+ return lines.join('\n');
1447
+ }
1448
+
1449
+ function runSurface(parsed, detectedRuntimeKeys) {
1450
+ if (parsed.surfaceAction === 'list') {
1451
+ const profiles = listSurfaceProfiles();
1452
+ console.log(parsed.installJson ? JSON.stringify(profiles, null, 2) : formatSurfaceList(profiles));
1453
+ return profiles;
1454
+ }
1455
+
1456
+ const surfaceState = readSurfaceSettings(parsed.isGlobal);
1457
+
1458
+ if (parsed.surfaceAction === 'status') {
1459
+ console.log(parsed.installJson ? JSON.stringify(surfaceState, null, 2) : formatSurfaceStatus(surfaceState));
1460
+ return surfaceState;
1461
+ }
1462
+
1463
+ const runtimeKeys = parsed.runtimeKeys.length > 0
1464
+ ? parsed.runtimeKeys
1465
+ : parsed.installDetected
1466
+ ? detectedRuntimeKeys
1467
+ : (surfaceState.settings?.runtimes && surfaceState.settings.runtimes.length > 0)
1468
+ ? surfaceState.settings.runtimes
1469
+ : detectedRuntimeKeys;
1470
+
1471
+ if (runtimeKeys.length === 0) {
1472
+ throw new Error('No runtime target found. Re-run with --runtimes <list> or --detected.');
1473
+ }
1474
+
1475
+ return runInstall({
1476
+ runtimeKeys,
1477
+ isGlobal: parsed.isGlobal ?? (surfaceState.settings?.scope !== 'project'),
1478
+ developerMode: parsed.developerMode ?? Boolean(surfaceState.settings?.developer_mode),
1479
+ silent: parsed.silent,
1480
+ installMode: 'non-interactive',
1481
+ profile: parsed.surfaceProfile,
1482
+ dryRun: parsed.installDryRun,
1483
+ json: parsed.installJson,
1484
+ });
1485
+ }
1486
+
1147
1487
  function resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY }) {
1148
1488
  const hasRuntimeDirective = parsed.runtimeKeys.length > 0 || parsed.installDetected;
1149
1489
  const hasModifierOverrides = parsed.isGlobal !== null || parsed.developerMode !== null;
@@ -1172,6 +1512,9 @@ function resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY }) {
1172
1512
  developerMode: parsed.developerMode ?? false,
1173
1513
  silent: parsed.silent,
1174
1514
  installMode: 'non-interactive',
1515
+ profile: parsed.installProfile,
1516
+ dryRun: parsed.installDryRun,
1517
+ json: parsed.installJson,
1175
1518
  };
1176
1519
  }
1177
1520
 
@@ -1180,6 +1523,9 @@ function resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY }) {
1180
1523
  isGlobal: parsed.isGlobal,
1181
1524
  developerMode: parsed.developerMode,
1182
1525
  hasModifierOverrides,
1526
+ profile: parsed.installProfile,
1527
+ dryRun: parsed.installDryRun,
1528
+ json: parsed.installJson,
1183
1529
  };
1184
1530
  }
1185
1531
 
@@ -1314,6 +1660,7 @@ const SETTINGS_SCHEMA = [
1314
1660
  { name: 'developer_mode', type: 'boolean', required: true, owned_by: 'user' },
1315
1661
  { name: 'data_dir', type: 'string', required: true, owned_by: 'installer' },
1316
1662
  { name: 'install_mode', type: 'string', required: true, enum: ['interactive', 'non-interactive'], owned_by: 'installer' },
1663
+ { name: 'profile', type: 'string', required: false, enum: Object.keys(SURFACE_PROFILES), owned_by: 'installer' },
1317
1664
  { name: 'installed_at', type: 'string', required: true, owned_by: 'installer' },
1318
1665
  ];
1319
1666
 
@@ -1347,6 +1694,9 @@ function migrateSettings(raw) {
1347
1694
  if (!Object.prototype.hasOwnProperty.call(out, 'install_mode') || out.install_mode === undefined) {
1348
1695
  out.install_mode = 'interactive';
1349
1696
  }
1697
+ if (!Object.prototype.hasOwnProperty.call(out, 'profile') || out.profile === undefined) {
1698
+ out.profile = DEFAULT_SURFACE_PROFILE;
1699
+ }
1350
1700
  return out;
1351
1701
  }
1352
1702
 
@@ -1609,6 +1959,11 @@ async function main() {
1609
1959
  }
1610
1960
 
1611
1961
  const detectedRuntimeKeys = Object.entries(RUNTIMES).filter(([, runtime]) => runtime.detect()).map(([key]) => key);
1962
+ if (parsed.command === 'surface') {
1963
+ runSurface(parsed, detectedRuntimeKeys);
1964
+ return;
1965
+ }
1966
+
1612
1967
  const installRequest = resolveInstallRequest(parsed, detectedRuntimeKeys, { isTTY: Boolean(process.stdin.isTTY) });
1613
1968
 
1614
1969
  if (installRequest.action === 'usage_error') {
@@ -1684,25 +2039,31 @@ async function main() {
1684
2039
  silent: false,
1685
2040
  detectedRuntimeKeys,
1686
2041
  installMode: 'interactive',
2042
+ profile: installRequest.profile,
2043
+ dryRun: installRequest.dryRun,
2044
+ json: installRequest.json,
1687
2045
  });
1688
2046
  }
1689
2047
 
1690
- function installCommandRuntime(runtime, isGlobal, log) {
2048
+ function installCommandRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) {
1691
2049
  const commandsDir = isGlobal ? runtime.commands_dir_global : path.resolve(runtime.commands_dir_project);
1692
2050
  const agentsDir = isGlobal ? runtime.agents_dir_global : path.resolve(runtime.agents_dir_project);
1693
- removePathIfExists(commandsDir);
2051
+ const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr');
2052
+ const commandEntries = collectInstallCommandEntries(profile);
2053
+ const removedCommandFiles = cleanMirroredFiles(commandsRoot, commandsDir);
1694
2054
  const removedAgentFiles = cleanMirroredFiles(path.join(PKG_ROOT, 'agents'), agentsDir);
1695
- const commandCount = copyDir(path.join(PKG_ROOT, 'commands', 'scr'), commandsDir);
2055
+ const commandCount = writeProfileCommandFiles(commandsRoot, commandsDir, commandEntries);
2056
+ writeSurfaceProfileMarker(commandsDir, profile);
1696
2057
  const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), agentsDir);
1697
- log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', commandsDir)}`);
2058
+ log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale files)`) : ''}`);
1698
2059
  log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', agentsDir)}${removedAgentFiles ? c('dim', ` (cleaned ${removedAgentFiles} stale files)`) : ''}`);
1699
2060
  }
1700
2061
 
1701
- function installClaudeCommandRuntime(runtime, isGlobal, log) {
2062
+ function installClaudeCommandRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) {
1702
2063
  const commandsDir = isGlobal ? runtime.commands_dir_global : path.resolve(runtime.commands_dir_project);
1703
2064
  const agentsDir = isGlobal ? runtime.agents_dir_global : path.resolve(runtime.agents_dir_project);
1704
2065
  const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr');
1705
- const commandEntries = collectCommandEntries(commandsRoot);
2066
+ const commandEntries = collectInstallCommandEntries(profile);
1706
2067
  const fileNames = commandEntries.map((entry) => commandEntryToFlatCommandFileName(entry));
1707
2068
 
1708
2069
  fs.mkdirSync(commandsDir, { recursive: true });
@@ -1718,23 +2079,31 @@ function installClaudeCommandRuntime(runtime, isGlobal, log) {
1718
2079
  }
1719
2080
 
1720
2081
  writeInstalledCommandManifest(commandsDir, 'claude-code', fileNames);
2082
+ writeSurfaceProfileMarker(commandsDir, profile);
1721
2083
  const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), agentsDir);
1722
2084
 
1723
- log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} /scr-* command files -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale items)`) : ''}`);
2085
+ log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} /scr-* command files (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale items)`) : ''}`);
1724
2086
  log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', agentsDir)}${removedAgentFiles ? c('dim', ` (cleaned ${removedAgentFiles} stale files)`) : ''}`);
1725
2087
  }
1726
2088
 
1727
- function installManifestSkillRuntime(runtime, isGlobal, log) {
2089
+ function installManifestSkillRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) {
1728
2090
  const skillsDir = isGlobal ? runtime.skills_dir_global : path.resolve(runtime.skills_dir_project);
1729
- removePathIfExists(skillsDir);
1730
- const manifest = generateSkillManifest(path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json'));
2091
+ const commandsRoot = path.join(PKG_ROOT, 'commands', 'scr');
2092
+ const commandEntries = collectInstallCommandEntries(profile);
2093
+ const manifest = generateSkillManifest(path.join(PKG_ROOT, 'data', 'CONSTRAINTS.json'), profile);
1731
2094
  fs.mkdirSync(skillsDir, { recursive: true });
2095
+ removePathIfExists(path.join(skillsDir, 'SKILL.md'));
2096
+ const commandsTarget = path.join(skillsDir, 'commands', 'scr');
2097
+ const agentsTarget = path.join(skillsDir, 'agents');
2098
+ cleanMirroredFiles(commandsRoot, commandsTarget);
2099
+ cleanMirroredFiles(path.join(PKG_ROOT, 'agents'), agentsTarget);
1732
2100
  atomicWriteFileSync(path.join(skillsDir, 'SKILL.md'), manifest);
1733
- const commandCount = copyDir(path.join(PKG_ROOT, 'commands', 'scr'), path.join(skillsDir, 'commands', 'scr'));
1734
- const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), path.join(skillsDir, 'agents'));
2101
+ const commandCount = writeProfileCommandFiles(commandsRoot, commandsTarget, commandEntries);
2102
+ writeSurfaceProfileMarker(skillsDir, profile);
2103
+ const agentCount = copyDir(path.join(PKG_ROOT, 'agents'), agentsTarget);
1735
2104
  log(` ${c('green', 'OK')} ${runtime.label}: SKILL.md manifest -> ${c('dim', path.join(skillsDir, 'SKILL.md'))}`);
1736
- log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', path.join(skillsDir, 'commands', 'scr'))}`);
1737
- log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', path.join(skillsDir, 'agents'))}`);
2105
+ log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', commandsTarget)}`);
2106
+ log(` ${c('green', 'OK')} ${runtime.label}: ${agentCount} agent prompts -> ${c('dim', agentsTarget)}`);
1738
2107
  }
1739
2108
 
1740
2109
  function installCodexAgentsWithMetadata(agentsDir) {
@@ -1757,17 +2126,15 @@ function installCodexAgentsWithMetadata(agentsDir) {
1757
2126
  };
1758
2127
  }
1759
2128
 
1760
- function installCodexRuntime(runtime, isGlobal, log) {
2129
+ function installCodexRuntime(runtime, isGlobal, log, profile = DEFAULT_SURFACE_PROFILE) {
1761
2130
  const skillsDir = isGlobal ? runtime.skills_dir_global : path.resolve(runtime.skills_dir_project);
1762
2131
  const commandsDir = isGlobal ? runtime.commands_dir_global : path.resolve(runtime.commands_dir_project);
1763
2132
  const agentsDir = isGlobal ? runtime.agents_dir_global : path.resolve(runtime.agents_dir_project);
1764
2133
  const sourceCommandsRoot = path.join(PKG_ROOT, 'commands', 'scr');
1765
- const commandEntries = collectCommandEntries(sourceCommandsRoot);
2134
+ const commandEntries = collectInstallCommandEntries(profile);
1766
2135
  const skillNames = commandEntries.map((entry) => entry.skillName);
1767
2136
 
1768
- // Wipe the installed commands dir so stale files from previous installs
1769
- // (removed commands, legacy flat layouts, etc.) do not linger.
1770
- removePathIfExists(commandsDir);
2137
+ const removedCommandFiles = cleanMirroredFiles(sourceCommandsRoot, commandsDir);
1771
2138
  fs.mkdirSync(skillsDir, { recursive: true });
1772
2139
  const removedSkillDirs = cleanCodexSkillDirs(skillsDir, skillNames);
1773
2140
 
@@ -1779,13 +2146,8 @@ function installCodexRuntime(runtime, isGlobal, log) {
1779
2146
  // clean content -- not on top of a previously-marked installed file -- so
1780
2147
  // re-runs are idempotent (single marker, current prose rewrite).
1781
2148
  let commandCount = 0;
1782
- for (const entry of commandEntries) {
1783
- const sourcePath = path.join(sourceCommandsRoot, entry.relativePath);
1784
- const sourceContent = fs.readFileSync(sourcePath, 'utf8');
1785
- const targetPath = path.join(commandsDir, entry.relativePath);
1786
- atomicWriteFileSync(targetPath, generateCodexCommandContent(entry, sourceContent));
1787
- commandCount++;
1788
- }
2149
+ commandCount = writeProfileCommandFiles(sourceCommandsRoot, commandsDir, commandEntries, generateCodexCommandContent);
2150
+ writeSurfaceProfileMarker(commandsDir, profile);
1789
2151
 
1790
2152
  const agentInstall = installCodexAgentsWithMetadata(agentsDir);
1791
2153
 
@@ -1796,13 +2158,14 @@ function installCodexRuntime(runtime, isGlobal, log) {
1796
2158
  atomicWriteFileSync(path.join(skillDir, 'SKILL.md'), generateCodexSkill(entry, commandPath));
1797
2159
  }
1798
2160
  writeCodexSkillManifest(skillsDir, skillNames);
2161
+ writeSurfaceProfileMarker(skillsDir, profile);
1799
2162
 
1800
- log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} \$scr-* skills -> ${c('dim', skillsDir)}${removedSkillDirs ? c('dim', ` (cleaned ${removedSkillDirs} stale dirs)`) : ''}`);
1801
- log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', commandsDir)}`);
2163
+ log(` ${c('green', 'OK')} ${runtime.label}: ${commandEntries.length} \$scr-* skills (${normalizeSurfaceProfile(profile)} profile) -> ${c('dim', skillsDir)}${removedSkillDirs ? c('dim', ` (cleaned ${removedSkillDirs} stale dirs)`) : ''}`);
2164
+ log(` ${c('green', 'OK')} ${runtime.label}: ${commandCount} command files -> ${c('dim', commandsDir)}${removedCommandFiles ? c('dim', ` (cleaned ${removedCommandFiles} stale files)`) : ''}`);
1802
2165
  log(` ${c('green', 'OK')} ${runtime.label}: ${agentInstall.agentCount} agent prompts + ${agentInstall.metadataCount} metadata files -> ${c('dim', agentsDir)}${agentInstall.removed ? c('dim', ` (cleaned ${agentInstall.removed} stale files)`) : ''}`);
1803
2166
  }
1804
2167
 
1805
- function installGuidedRuntime(runtime, isGlobal, dataDir, log) {
2168
+ function installGuidedRuntime(runtime, isGlobal, dataDir, log, profile = DEFAULT_SURFACE_PROFILE) {
1806
2169
  const guideDir = isGlobal ? runtime.guide_dir_global : path.resolve(runtime.guide_dir_project);
1807
2170
  const currentProjectDir = path.resolve('.');
1808
2171
  const setupGuide = generatePerplexitySetupGuide({
@@ -1821,12 +2184,13 @@ function installGuidedRuntime(runtime, isGlobal, dataDir, log) {
1821
2184
  atomicWriteFileSync(path.join(guideDir, 'SETUP.md'), setupGuide);
1822
2185
  atomicWriteFileSync(path.join(guideDir, 'connector-command.txt'), connectorCommand + '\n');
1823
2186
  atomicWriteFileSync(path.join(guideDir, 'connector-command.current-project.txt'), currentProjectCommand + '\n');
2187
+ writeSurfaceProfileMarker(guideDir, profile);
1824
2188
 
1825
2189
  log(` ${c('green', 'OK')} ${runtime.label}: setup guide -> ${c('dim', path.join(guideDir, 'SETUP.md'))}`);
1826
2190
  log(` ${c('green', 'OK')} ${runtime.label}: connector recipe -> ${c('dim', path.join(guideDir, 'connector-command.txt'))}`);
1827
2191
  }
1828
2192
 
1829
- function writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log) {
2193
+ function writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log, profile = DEFAULT_SURFACE_PROFILE) {
1830
2194
  fs.mkdirSync(path.join(dataDir, 'templates'), { recursive: true });
1831
2195
  fs.mkdirSync(path.join(dataDir, 'data'), { recursive: true });
1832
2196
  fs.mkdirSync(path.join(dataDir, 'lib'), { recursive: true });
@@ -1873,6 +2237,7 @@ function writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, instal
1873
2237
  developer_mode: developerMode,
1874
2238
  data_dir: dataDir,
1875
2239
  install_mode: installMode,
2240
+ profile: normalizeSurfaceProfile(profile),
1876
2241
  installed_at: new Date().toISOString(),
1877
2242
  };
1878
2243
  const mergedSettings = mergeSettings(existingSettings, incomingSettings);
@@ -1929,14 +2294,101 @@ function collectTargetDirsForSweep(runtimeKeys, isGlobal, dataDir) {
1929
2294
  return Array.from(dirs);
1930
2295
  }
1931
2296
 
1932
- function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode }) {
2297
+ function runtimeInstallTargets(runtimeKey, runtime, isGlobal) {
2298
+ const resolve = (globalPath, projectPath) => isGlobal ? globalPath : (projectPath ? path.resolve(projectPath) : null);
2299
+ return {
2300
+ runtime: runtimeKey,
2301
+ label: runtime.label,
2302
+ type: runtime.type,
2303
+ commandsDir: resolve(runtime.commands_dir_global, runtime.commands_dir_project),
2304
+ skillsDir: resolve(runtime.skills_dir_global, runtime.skills_dir_project),
2305
+ agentsDir: resolve(runtime.agents_dir_global, runtime.agents_dir_project),
2306
+ guideDir: resolve(runtime.guide_dir_global, runtime.guide_dir_project),
2307
+ };
2308
+ }
2309
+
2310
+ function buildInstallDryRun({ runtimeKeys, isGlobal, developerMode, installMode, profile = DEFAULT_SURFACE_PROFILE }) {
2311
+ const resolvedProfile = normalizeSurfaceProfile(profile);
1933
2312
  const dataDir = isGlobal ? path.join(os.homedir(), '.scriveno') : path.resolve('.scriveno');
1934
- const log = silent ? () => {} : (message) => console.log(message);
2313
+ const commandEntries = collectInstallCommandEntries(resolvedProfile);
2314
+ const profileSummary = surfaceProfileSummary(resolvedProfile);
2315
+ const agentCount = collectAgentEntries(path.join(PKG_ROOT, 'agents')).length;
2316
+ const sharedAssets = {
2317
+ templates: listRelativeFiles(path.join(PKG_ROOT, 'templates')).length,
2318
+ data: listRelativeFiles(path.join(PKG_ROOT, 'data')).length,
2319
+ lib: listRelativeFiles(path.join(PKG_ROOT, 'lib')).length,
2320
+ };
2321
+
2322
+ return {
2323
+ version: VERSION,
2324
+ profile: resolvedProfile,
2325
+ profileLabel: profileSummary.label,
2326
+ profileDescription: profileSummary.description,
2327
+ registeredCommands: profileSummary.commandCount,
2328
+ commandFiles: commandEntries.length,
2329
+ agentPrompts: agentCount,
2330
+ scope: isGlobal ? 'global' : 'project',
2331
+ mode: developerMode ? 'developer' : 'writer',
2332
+ installMode,
2333
+ dataDir,
2334
+ sharedAssets,
2335
+ targets: runtimeKeys.map((runtimeKey) => {
2336
+ const runtime = RUNTIMES[runtimeKey];
2337
+ if (!runtime) throw new Error(`Unknown runtime "${runtimeKey}"`);
2338
+ return runtimeInstallTargets(runtimeKey, runtime, isGlobal);
2339
+ }),
2340
+ writes: false,
2341
+ };
2342
+ }
2343
+
2344
+ function formatInstallDryRunReport(plan) {
2345
+ const lines = [
2346
+ 'Scriveno install dry run',
2347
+ `Version: ${plan.version}`,
2348
+ `Profile: ${plan.profile} (${plan.profileLabel})`,
2349
+ `Scope: ${plan.scope}`,
2350
+ `Mode: ${plan.mode}`,
2351
+ `Command files selected: ${plan.commandFiles}`,
2352
+ `Registered command entries selected: ${plan.registeredCommands}`,
2353
+ `Agent prompts selected: ${plan.agentPrompts}`,
2354
+ `Shared assets: ${plan.sharedAssets.templates} templates, ${plan.sharedAssets.data} data files, ${plan.sharedAssets.lib} lib files`,
2355
+ `Data directory: ${plan.dataDir}`,
2356
+ '',
2357
+ 'Runtime targets:',
2358
+ ];
2359
+ for (const target of plan.targets) {
2360
+ lines.push(`- ${target.label} (${target.runtime})`);
2361
+ if (target.commandsDir) lines.push(` commands: ${target.commandsDir}`);
2362
+ if (target.skillsDir) lines.push(` skills: ${target.skillsDir}`);
2363
+ if (target.agentsDir) lines.push(` agents: ${target.agentsDir}`);
2364
+ if (target.guideDir) lines.push(` guide: ${target.guideDir}`);
2365
+ }
2366
+ lines.push('');
2367
+ lines.push('No files were written.');
2368
+ return lines.join('\n');
2369
+ }
2370
+
2371
+ function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode, profile = DEFAULT_SURFACE_PROFILE, dryRun = false, json = false }) {
2372
+ const resolvedProfile = normalizeSurfaceProfile(profile);
2373
+ const dataDir = isGlobal ? path.join(os.homedir(), '.scriveno') : path.resolve('.scriveno');
2374
+ const log = (silent || json || dryRun) ? () => {} : (message) => console.log(message);
1935
2375
 
1936
2376
  if (!runtimeKeys.length) {
1937
2377
  throw new Error('No runtimes selected for installation');
1938
2378
  }
1939
2379
 
2380
+ if (dryRun) {
2381
+ const plan = buildInstallDryRun({
2382
+ runtimeKeys,
2383
+ isGlobal,
2384
+ developerMode,
2385
+ installMode,
2386
+ profile: resolvedProfile,
2387
+ });
2388
+ console.log(json ? JSON.stringify(plan, null, 2) : formatInstallDryRunReport(plan));
2389
+ return plan;
2390
+ }
2391
+
1940
2392
  let totalOrphansRemoved = 0;
1941
2393
  for (const dir of collectTargetDirsForSweep(runtimeKeys, isGlobal, dataDir)) {
1942
2394
  totalOrphansRemoved += cleanOrphanedTempFiles(dir);
@@ -1945,7 +2397,7 @@ function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode
1945
2397
  log(c('dim', ` Cleaned ${totalOrphansRemoved} orphaned temp file(s) from prior interrupted install`));
1946
2398
  }
1947
2399
 
1948
- if (!silent) {
2400
+ if (!silent && !json) {
1949
2401
  console.log('\n' + c('bold', 'Installing...'));
1950
2402
  }
1951
2403
 
@@ -1955,27 +2407,42 @@ function runInstall({ runtimeKeys, isGlobal, developerMode, silent, installMode
1955
2407
  throw new Error(`Unknown runtime "${runtimeKey}"`);
1956
2408
  }
1957
2409
  if (runtimeKey === 'codex') {
1958
- installCodexRuntime(runtime, isGlobal, log);
2410
+ installCodexRuntime(runtime, isGlobal, log, resolvedProfile);
1959
2411
  } else if (runtime.command_layout === 'flat-prefixed') {
1960
- installClaudeCommandRuntime(runtime, isGlobal, log);
2412
+ installClaudeCommandRuntime(runtime, isGlobal, log, resolvedProfile);
1961
2413
  } else if (runtime.type === 'skills') {
1962
- installManifestSkillRuntime(runtime, isGlobal, log);
2414
+ installManifestSkillRuntime(runtime, isGlobal, log, resolvedProfile);
1963
2415
  } else if (runtime.type === 'guided-mcp') {
1964
- installGuidedRuntime(runtime, isGlobal, dataDir, log);
2416
+ installGuidedRuntime(runtime, isGlobal, dataDir, log, resolvedProfile);
1965
2417
  } else {
1966
- installCommandRuntime(runtime, isGlobal, log);
2418
+ installCommandRuntime(runtime, isGlobal, log, resolvedProfile);
1967
2419
  }
1968
2420
  }
1969
2421
 
1970
- writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log);
2422
+ writeSharedAssets(dataDir, runtimeKeys, isGlobal, developerMode, installMode, log, resolvedProfile);
2423
+
2424
+ const summary = {
2425
+ version: VERSION,
2426
+ runtimes: runtimeKeys,
2427
+ scope: isGlobal ? 'global' : 'project',
2428
+ mode: developerMode ? 'developer' : 'writer',
2429
+ profile: resolvedProfile,
2430
+ dataDir,
2431
+ };
2432
+
2433
+ if (json) {
2434
+ console.log(JSON.stringify(summary, null, 2));
2435
+ return summary;
2436
+ }
1971
2437
 
1972
2438
  if (silent) {
1973
- console.log(`Installed Scriveno ${VERSION} to ${runtimeKeys.join(', ')} (${isGlobal ? 'global' : 'project'}, ${developerMode ? 'developer' : 'writer'} mode).`);
1974
- return;
2439
+ console.log(`Installed Scriveno ${VERSION} to ${runtimeKeys.join(', ')} (${isGlobal ? 'global' : 'project'}, ${developerMode ? 'developer' : 'writer'} mode, ${resolvedProfile} profile).`);
2440
+ return summary;
1975
2441
  }
1976
2442
 
1977
2443
  console.log('\n' + c('bold', c('green', 'Installation complete!')));
1978
2444
  printNextSteps(runtimeKeys);
2445
+ return summary;
1979
2446
  }
1980
2447
 
1981
2448
  // Only run interactive installer when executed directly
@@ -1990,14 +2457,19 @@ if (require.main === module) {
1990
2457
  module.exports = {
1991
2458
  copyDir,
1992
2459
  RUNTIMES,
2460
+ SURFACE_PROFILES,
2461
+ DEFAULT_SURFACE_PROFILE,
1993
2462
  parseArgs,
1994
2463
  resolveInstallRequest,
2464
+ runInstall,
1995
2465
  runStatus,
1996
2466
  runSyncCheck,
1997
2467
  runRuntimeSmoke,
1998
2468
  runAgentAvailability,
1999
2469
  runRouteAudit,
2000
2470
  collectCommandEntries,
2471
+ collectCommandEntriesForProfile,
2472
+ collectInstallCommandEntries,
2001
2473
  collectAgentEntries,
2002
2474
  assertNoSkillNameCollisions,
2003
2475
  cleanCodexSkillDirs,
@@ -2015,9 +2487,16 @@ module.exports = {
2015
2487
  installManifestSkillRuntime,
2016
2488
  installCodexRuntime,
2017
2489
  installCodexAgentsWithMetadata,
2490
+ runSurface,
2018
2491
  cleanFlatCommandFiles,
2019
2492
  generateCodexSkill,
2020
2493
  generateSkillManifest,
2494
+ normalizeSurfaceProfile,
2495
+ resolveProfileCommandKeys,
2496
+ surfaceProfileSummary,
2497
+ listSurfaceProfiles,
2498
+ buildInstallDryRun,
2499
+ formatInstallDryRunReport,
2021
2500
  buildFilesystemMcpCommand,
2022
2501
  generatePerplexitySetupGuide,
2023
2502
  atomicWriteFileSync,