@factorialco/fcode-cli 2.4.0 → 3.0.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 (114) hide show
  1. package/README.md +331 -118
  2. package/dist/commands/clone.d.ts +0 -12
  3. package/dist/commands/clone.js +22 -135
  4. package/dist/commands/{settings → config}/get.d.ts +1 -1
  5. package/dist/commands/{settings → config}/get.js +7 -7
  6. package/dist/commands/{settings → config}/remove.d.ts +1 -1
  7. package/dist/commands/{settings → config}/remove.js +6 -6
  8. package/dist/commands/{settings → config}/set.d.ts +1 -1
  9. package/dist/commands/{settings → config}/set.js +8 -8
  10. package/dist/commands/dependencies/diff.d.ts +9 -0
  11. package/dist/commands/dependencies/diff.js +15 -0
  12. package/dist/commands/dependencies/pull.d.ts +9 -0
  13. package/dist/commands/dependencies/pull.js +44 -1
  14. package/dist/commands/diff.d.ts +12 -0
  15. package/dist/commands/diff.js +48 -0
  16. package/dist/commands/http.d.ts +1 -1
  17. package/dist/commands/http.js +7 -7
  18. package/dist/commands/i18n/diff.d.ts +9 -0
  19. package/dist/commands/i18n/diff.js +15 -0
  20. package/dist/commands/i18n/status.d.ts +3 -0
  21. package/dist/commands/i18n/status.js +1 -0
  22. package/dist/commands/modules/diff.d.ts +9 -0
  23. package/dist/commands/modules/diff.js +15 -0
  24. package/dist/commands/modules/status.d.ts +3 -0
  25. package/dist/commands/modules/status.js +1 -0
  26. package/dist/commands/processes/diff.d.ts +9 -0
  27. package/dist/commands/processes/diff.js +15 -0
  28. package/dist/commands/processes/status.d.ts +3 -0
  29. package/dist/commands/processes/status.js +1 -0
  30. package/dist/commands/pull.js +2 -2
  31. package/dist/commands/push.js +2 -2
  32. package/dist/commands/{team → settings}/aliases/delete.d.ts +1 -1
  33. package/dist/commands/{team → settings}/aliases/delete.js +6 -6
  34. package/dist/commands/{team → settings}/aliases/list.d.ts +1 -1
  35. package/dist/commands/{team → settings}/aliases/list.js +8 -8
  36. package/dist/commands/{team → settings}/aliases/set.d.ts +1 -1
  37. package/dist/commands/{team → settings}/aliases/set.js +8 -8
  38. package/dist/commands/settings/diff.d.ts +8 -0
  39. package/dist/commands/settings/diff.js +39 -0
  40. package/dist/commands/settings/pull.d.ts +11 -0
  41. package/dist/commands/settings/pull.js +41 -0
  42. package/dist/commands/{team → settings}/push.d.ts +4 -4
  43. package/dist/commands/{team → settings}/push.js +39 -39
  44. package/dist/commands/settings/status.d.ts +8 -0
  45. package/dist/commands/settings/status.js +46 -0
  46. package/dist/commands/{team → settings}/versions/create.d.ts +1 -1
  47. package/dist/commands/{team → settings}/versions/create.js +13 -10
  48. package/dist/commands/{team → settings}/versions/delete.d.ts +1 -1
  49. package/dist/commands/{team → settings}/versions/delete.js +10 -10
  50. package/dist/commands/{team → settings}/versions/list.d.ts +1 -1
  51. package/dist/commands/{team → settings}/versions/list.js +8 -8
  52. package/dist/commands/status.d.ts +3 -0
  53. package/dist/commands/status.js +10 -7
  54. package/dist/commands/team/clone.d.ts +14 -0
  55. package/dist/commands/team/clone.js +91 -0
  56. package/dist/commands/team/pull.d.ts +3 -7
  57. package/dist/commands/team/pull.js +25 -30
  58. package/dist/commands/team/status.d.ts +3 -4
  59. package/dist/commands/team/status.js +34 -33
  60. package/dist/commands/variables/diff.d.ts +9 -0
  61. package/dist/commands/variables/diff.js +15 -0
  62. package/dist/commands/variables/status.d.ts +3 -0
  63. package/dist/commands/variables/status.js +1 -0
  64. package/dist/config/dev-team.d.ts +48 -0
  65. package/dist/config/dev-team.js +62 -0
  66. package/dist/config/workspace.d.ts +2 -2
  67. package/dist/config/workspace.js +4 -2
  68. package/dist/core/commands/base-command.js +5 -0
  69. package/dist/core/commands/dependency-command.d.ts +2 -1
  70. package/dist/core/commands/dependency-command.js +3 -0
  71. package/dist/core/commands/i18n-command.js +2 -0
  72. package/dist/core/commands/modules-command.js +2 -0
  73. package/dist/core/commands/processes-command.js +2 -0
  74. package/dist/core/commands/resource-command.d.ts +26 -1
  75. package/dist/core/commands/resource-command.js +100 -3
  76. package/dist/core/commands/variables-command.d.ts +7 -0
  77. package/dist/core/commands/variables-command.js +13 -0
  78. package/dist/core/executors/javascript-execution-task.js +25 -3
  79. package/dist/core/types.d.ts +30 -7
  80. package/dist/core/types.js +38 -10
  81. package/dist/repositories/process.d.ts +8 -2
  82. package/dist/repositories/process.js +28 -18
  83. package/dist/repositories/programming-language-manifest.d.ts +15 -1
  84. package/dist/repositories/programming-language-manifest.js +45 -1
  85. package/dist/repositories/settings.d.ts +52 -0
  86. package/dist/repositories/settings.js +214 -0
  87. package/dist/runtime/libs/python/fcode/context.py +29 -2
  88. package/dist/services/dashboard-client.d.ts +31 -0
  89. package/dist/services/dashboard-client.js +99 -0
  90. package/dist/services/dev-team-service.d.ts +41 -0
  91. package/dist/services/dev-team-service.js +162 -0
  92. package/dist/services/fcode-graphql-client.d.ts +16 -2
  93. package/dist/services/fcode-graphql-client.js +56 -7
  94. package/dist/services/locale-resolver.d.ts +16 -5
  95. package/dist/services/locale-resolver.js +60 -12
  96. package/dist/services/resource-differ.d.ts +60 -0
  97. package/dist/services/resource-differ.js +135 -0
  98. package/dist/services/skills-installer.d.ts +16 -0
  99. package/dist/services/skills-installer.js +77 -0
  100. package/dist/services/team-versioning.d.ts +4 -4
  101. package/dist/services/team-versioning.js +4 -4
  102. package/dist/services/webhook-auth.js +1 -1
  103. package/dist/services/workspace-cloner.d.ts +31 -0
  104. package/dist/services/workspace-cloner.js +95 -0
  105. package/dist/utils/dev-slug.d.ts +12 -0
  106. package/dist/utils/dev-slug.js +37 -0
  107. package/dist/utils/diff.d.ts +19 -0
  108. package/dist/utils/diff.js +58 -0
  109. package/dist/utils/slugify.d.ts +17 -0
  110. package/dist/utils/slugify.js +41 -0
  111. package/oclif.manifest.json +428 -116
  112. package/package.json +12 -7
  113. package/dist/repositories/team.d.ts +0 -41
  114. package/dist/repositories/team.js +0 -170
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Installs the Factorial Code agent skills into `targetDir`. The skills CLI writes the skills
3
+ * themselves under `.agents/skills/` and symlinks them from `.claude/skills/`; it only installs
4
+ * to agents it detects, so `.claude/` is created first to make sure Claude Code is targeted.
5
+ */
6
+ export declare function installSkillsAt(targetDir: string): Promise<void>;
7
+ export declare function skillsInstalledAt(rootDir: string): boolean;
8
+ /**
9
+ * Points `targetDir`'s `.claude/skills` at the ones installed in `rootDir`, so a team clone
10
+ * installs the skills once and every App still resolves them.
11
+ *
12
+ * The link is relative, so moving the whole tree keeps it working, and the per-skill symlinks
13
+ * inside it resolve against the real directory — not against the link — so they stay valid.
14
+ * Returns false when there is nothing to link to (the install failed, and it is not fatal).
15
+ */
16
+ export declare function linkSkillsInto(targetDir: string, rootDir: string): boolean;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installSkillsAt = installSkillsAt;
4
+ exports.skillsInstalledAt = skillsInstalledAt;
5
+ exports.linkSkillsInto = linkSkillsInto;
6
+ const node_child_process_1 = require("node:child_process");
7
+ const fs = require("fs-extra");
8
+ const path = require("node:path");
9
+ const cli_1 = require("../utils/cli");
10
+ // Skill sources installed on clone. `release-sdk` in factorial-api-sdks is a
11
+ // maintainer-only skill, so we pick `factorial-api-sdks` explicitly there.
12
+ const SKILLS_SOURCES = [
13
+ { repo: 'factorialco/factorial-code-skills', skills: '*' },
14
+ { repo: 'factorialco/factorial-api-sdks', skills: 'factorial-api-sdks' },
15
+ ];
16
+ /** Where the skills CLI symlinks the installed skills for Claude Code to find them. */
17
+ const CLAUDE_SKILLS_PATH = path.join('.claude', 'skills');
18
+ function skillsInstallHint(repo, skills) {
19
+ cli_1.default.log('💡 Install the Factorial Code agent skills later with:');
20
+ cli_1.default.log(` npx skills add ${repo} -s '${skills}' -y`);
21
+ }
22
+ async function installSkills(repo, skills, cwd) {
23
+ // `npx --yes skills add <repo> -s <skills> -y` — non-interactive install with
24
+ // the skills CLI defaults: project scope (no -g), symlinked (no --copy).
25
+ const skillsArgs = ['--yes', 'skills', 'add', repo, '-s', skills, '-y'];
26
+ await new Promise((resolve) => {
27
+ const child = (0, node_child_process_1.spawn)('npx', skillsArgs, { cwd, stdio: 'inherit' });
28
+ child.on('error', () => {
29
+ // npx not available (or failed to launch) — don't fail the clone, just tell the user how to do it.
30
+ cli_1.default.log("\n⚠️ Couldn't run npx automatically (is Node.js / npx installed?).");
31
+ skillsInstallHint(repo, skills);
32
+ resolve();
33
+ });
34
+ child.on('close', (code) => {
35
+ if (code !== 0) {
36
+ cli_1.default.log("\n⚠️ Skill installation didn't complete.");
37
+ skillsInstallHint(repo, skills);
38
+ }
39
+ resolve();
40
+ });
41
+ });
42
+ }
43
+ /**
44
+ * Installs the Factorial Code agent skills into `targetDir`. The skills CLI writes the skills
45
+ * themselves under `.agents/skills/` and symlinks them from `.claude/skills/`; it only installs
46
+ * to agents it detects, so `.claude/` is created first to make sure Claude Code is targeted.
47
+ */
48
+ async function installSkillsAt(targetDir) {
49
+ cli_1.default.log('');
50
+ fs.ensureDirSync(path.join(targetDir, '.claude'));
51
+ for (const source of SKILLS_SOURCES) {
52
+ await installSkills(source.repo, source.skills, targetDir);
53
+ }
54
+ }
55
+ function skillsInstalledAt(rootDir) {
56
+ return fs.pathExistsSync(path.join(rootDir, CLAUDE_SKILLS_PATH));
57
+ }
58
+ /**
59
+ * Points `targetDir`'s `.claude/skills` at the ones installed in `rootDir`, so a team clone
60
+ * installs the skills once and every App still resolves them.
61
+ *
62
+ * The link is relative, so moving the whole tree keeps it working, and the per-skill symlinks
63
+ * inside it resolve against the real directory — not against the link — so they stay valid.
64
+ * Returns false when there is nothing to link to (the install failed, and it is not fatal).
65
+ */
66
+ function linkSkillsInto(targetDir, rootDir) {
67
+ if (!skillsInstalledAt(rootDir)) {
68
+ return false;
69
+ }
70
+ const link = path.join(targetDir, CLAUDE_SKILLS_PATH);
71
+ if (fs.pathExistsSync(link) || fs.lstatSync(link, { throwIfNoEntry: false })) {
72
+ return true;
73
+ }
74
+ fs.ensureDirSync(path.dirname(link));
75
+ fs.symlinkSync(path.relative(path.dirname(link), path.join(rootDir, CLAUDE_SKILLS_PATH)), link, 'dir');
76
+ return true;
77
+ }
@@ -1,6 +1,6 @@
1
- import { Team, TeamAliasFileEntry, TeamVersion, TeamVersionAlias, TeamVersionFileEntry } from '../core/types';
1
+ import { TeamAliasFileEntry, TeamVersion, TeamVersionAlias, TeamVersionFileEntry, WorkspaceSettings } from '../core/types';
2
2
  /**
3
- * What a team:push must do to make the cloud's team versions and aliases match team.json.
3
+ * What a settings:push must do to make the cloud's versions and aliases match settings.json.
4
4
  * Versions are only ever created or deleted (tags are immutable snapshots); aliases are
5
5
  * name->tag pointers, so a changed tag is an upsert of the same name.
6
6
  */
@@ -10,7 +10,7 @@ export interface TeamVersioningSyncPlan {
10
10
  setAliases: TeamAliasFileEntry[];
11
11
  deleteAliases: TeamVersionAlias[];
12
12
  }
13
- export declare function planTeamVersioningSync(team: Team, cloudVersions: TeamVersion[], cloudAliases: TeamVersionAlias[]): TeamVersioningSyncPlan;
13
+ export declare function planTeamVersioningSync(settings: WorkspaceSettings, cloudVersions: TeamVersion[], cloudAliases: TeamVersionAlias[]): TeamVersioningSyncPlan;
14
14
  export declare function teamVersioningSyncPlanIsEmpty(plan: TeamVersioningSyncPlan): boolean;
15
- /** Logs the per-entity outcome of publishing a team version and returns how many failed. */
15
+ /** Logs the per-entity outcome of publishing a workspace version and returns how many failed. */
16
16
  export declare function logTeamVersionManifest(teamVersion: TeamVersion): number;
@@ -5,9 +5,9 @@ exports.teamVersioningSyncPlanIsEmpty = teamVersioningSyncPlanIsEmpty;
5
5
  exports.logTeamVersionManifest = logTeamVersionManifest;
6
6
  const color_1 = require("@oclif/color");
7
7
  const cli_1 = require("../utils/cli");
8
- function planTeamVersioningSync(team, cloudVersions, cloudAliases) {
9
- const localVersions = team.versions ?? [];
10
- const localAliases = team.aliases ?? [];
8
+ function planTeamVersioningSync(settings, cloudVersions, cloudAliases) {
9
+ const localVersions = settings.versions ?? [];
10
+ const localAliases = settings.aliases ?? [];
11
11
  const cloudVersionTags = new Set(cloudVersions.map(({ tag }) => tag));
12
12
  const localVersionTags = new Set(localVersions.map(({ tag }) => tag));
13
13
  const cloudAliasesByName = new Map(cloudAliases.map((alias) => [alias.name, alias]));
@@ -25,7 +25,7 @@ function teamVersioningSyncPlanIsEmpty(plan) {
25
25
  plan.setAliases.length === 0 &&
26
26
  plan.deleteAliases.length === 0);
27
27
  }
28
- /** Logs the per-entity outcome of publishing a team version and returns how many failed. */
28
+ /** Logs the per-entity outcome of publishing a workspace version and returns how many failed. */
29
29
  function logTeamVersionManifest(teamVersion) {
30
30
  const created = teamVersion.manifest.filter(({ outcome }) => outcome === 'CREATED');
31
31
  const skipped = teamVersion.manifest.filter(({ outcome }) => outcome === 'SKIPPED_EXISTS');
@@ -37,7 +37,7 @@ function resolveEffectiveAuth(webhook, teamWebhookAuth) {
37
37
  }
38
38
  if (authMode === 'TEAM') {
39
39
  if (!teamWebhookAuth) {
40
- throw new Error('this webhook inherits the workspace webhook auth, which is not set in team.json');
40
+ throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json');
41
41
  }
42
42
  return teamWebhookAuth;
43
43
  }
@@ -0,0 +1,31 @@
1
+ import { TeamOrigin } from '../core/types';
2
+ /**
3
+ * Creating and syncing a workspace directory, independent of the command that asks for it, so
4
+ * `fcode clone` (one workspace) and `fcode team:clone` (one per App) share the same behaviour.
5
+ *
6
+ * Everything below a resource command resolves the workspace from `process.cwd()` — the
7
+ * repositories, `WorkspaceConfig`, and the GraphQL client bound to the workspace slug through the
8
+ * `Fcode-Team` header. `runInWorkspace` is therefore the seam: it moves into the directory,
9
+ * runs the command, and always moves back. Commands run sequentially and each `Command.run()`
10
+ * builds a fresh instance, so every workspace gets its own config and its own client.
11
+ */
12
+ /** Runs `body` with the process rooted at `workspaceDir`, restoring the previous cwd afterwards. */
13
+ export declare function runInWorkspace<T>(workspaceDir: string, body: () => Promise<T>): Promise<T>;
14
+ export declare class WorkspaceNotEmpty extends Error {
15
+ readonly workspaceDir: string;
16
+ constructor(workspaceDir: string);
17
+ }
18
+ export declare class WorkspaceAlreadyCloned extends Error {
19
+ readonly workspaceDir: string;
20
+ constructor(workspaceDir: string);
21
+ }
22
+ /** True when the directory already holds a Factorial Code workspace (`.fcode/workspace.json`). */
23
+ export declare function isClonedWorkspace(workspaceDir: string): Promise<boolean>;
24
+ /**
25
+ * Lays out an empty workspace at `workspaceDir`: the resource directories, the `.fcode` metadata
26
+ * pointing at `origin`, and the files a pull expects to already exist.
27
+ *
28
+ * Throws `WorkspaceAlreadyCloned` / `WorkspaceNotEmpty` rather than exiting, so a caller cloning
29
+ * many workspaces can report the failure and carry on with the rest.
30
+ */
31
+ export declare function scaffoldWorkspace(workspaceDir: string, origin: TeamOrigin): Promise<void>;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkspaceAlreadyCloned = exports.WorkspaceNotEmpty = void 0;
4
+ exports.runInWorkspace = runInWorkspace;
5
+ exports.isClonedWorkspace = isClonedWorkspace;
6
+ exports.scaffoldWorkspace = scaffoldWorkspace;
7
+ const fs = require("fs-extra");
8
+ const path = require("node:path");
9
+ const global_1 = require("../config/global");
10
+ const workspace_1 = require("../config/workspace");
11
+ const settings_1 = require("../config/settings");
12
+ /**
13
+ * Creating and syncing a workspace directory, independent of the command that asks for it, so
14
+ * `fcode clone` (one workspace) and `fcode team:clone` (one per App) share the same behaviour.
15
+ *
16
+ * Everything below a resource command resolves the workspace from `process.cwd()` — the
17
+ * repositories, `WorkspaceConfig`, and the GraphQL client bound to the workspace slug through the
18
+ * `Fcode-Team` header. `runInWorkspace` is therefore the seam: it moves into the directory,
19
+ * runs the command, and always moves back. Commands run sequentially and each `Command.run()`
20
+ * builds a fresh instance, so every workspace gets its own config and its own client.
21
+ */
22
+ /** Runs `body` with the process rooted at `workspaceDir`, restoring the previous cwd afterwards. */
23
+ async function runInWorkspace(workspaceDir, body) {
24
+ const previousCwd = process.cwd();
25
+ process.chdir(workspaceDir);
26
+ try {
27
+ return await body();
28
+ }
29
+ finally {
30
+ process.chdir(previousCwd);
31
+ }
32
+ }
33
+ class WorkspaceNotEmpty extends Error {
34
+ constructor(workspaceDir) {
35
+ super(`Destination path '${workspaceDir}' already exists and is not empty.`);
36
+ this.workspaceDir = workspaceDir;
37
+ }
38
+ }
39
+ exports.WorkspaceNotEmpty = WorkspaceNotEmpty;
40
+ class WorkspaceAlreadyCloned extends Error {
41
+ constructor(workspaceDir) {
42
+ super(`Workspace '${workspaceDir}' already cloned.`);
43
+ this.workspaceDir = workspaceDir;
44
+ }
45
+ }
46
+ exports.WorkspaceAlreadyCloned = WorkspaceAlreadyCloned;
47
+ /** True when the directory already holds a Factorial Code workspace (`.fcode/workspace.json`). */
48
+ async function isClonedWorkspace(workspaceDir) {
49
+ try {
50
+ await new workspace_1.default(workspaceDir).getActiveTeam();
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ async function ignoreWorkspaceNotEmpty() {
58
+ const envValue = process.env.IGNORE_WORKSPACE_NOT_EMPTY;
59
+ if (envValue === 'true' || envValue === 'false') {
60
+ return envValue === 'true';
61
+ }
62
+ return (await new global_1.default().getSetting(settings_1.SettingKeys.IGNORE_WORKSPACE_NOT_EMPTY)) === 'true';
63
+ }
64
+ /**
65
+ * Lays out an empty workspace at `workspaceDir`: the resource directories, the `.fcode` metadata
66
+ * pointing at `origin`, and the files a pull expects to already exist.
67
+ *
68
+ * Throws `WorkspaceAlreadyCloned` / `WorkspaceNotEmpty` rather than exiting, so a caller cloning
69
+ * many workspaces can report the failure and carry on with the rest.
70
+ */
71
+ async function scaffoldWorkspace(workspaceDir, origin) {
72
+ if (fs.pathExistsSync(workspaceDir)) {
73
+ // Listed up front, and `.fcode` probed only when it is already there: constructing a
74
+ // WorkspaceConfig creates the directory, which would make an empty target read as occupied.
75
+ const entries = fs.readdirSync(workspaceDir);
76
+ if (entries.includes('.fcode') && (await isClonedWorkspace(workspaceDir))) {
77
+ throw new WorkspaceAlreadyCloned(workspaceDir);
78
+ }
79
+ if (entries.length > 0 && !(await ignoreWorkspaceNotEmpty())) {
80
+ throw new WorkspaceNotEmpty(workspaceDir);
81
+ }
82
+ }
83
+ const globalConfig = new global_1.default();
84
+ const variablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.VARIABLES_FILE_NAME);
85
+ const localVariablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.LOCAL_VARIABLES_FILE_NAME);
86
+ fs.mkdirpSync(workspaceDir);
87
+ for (const directory of ['processes', 'modules', 'dependencies', 'i18n', 'storage']) {
88
+ fs.mkdirpSync(path.join(workspaceDir, directory));
89
+ }
90
+ fs.createFileSync(path.join(workspaceDir, variablesFileName));
91
+ fs.outputFileSync(path.join(workspaceDir, localVariablesFileName), '');
92
+ fs.outputFileSync(path.join(workspaceDir, 'datastore.json'), '{}');
93
+ fs.outputFileSync(path.join(workspaceDir, '.gitignore'), [localVariablesFileName, 'datastore.json', '.env', 'storage/*'].join('\n'));
94
+ await new workspace_1.default(workspaceDir).init(origin);
95
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Workspace slugs embed an App's UUID base36-encoded: the platform derives a Postgres database
3
+ * name from the slug and a raw UUID overflows the 63-char identifier limit, so 128 bits become
4
+ * 25 lowercase alphanumeric characters.
5
+ *
6
+ * Ported from the dashboard's `lib/slug.ts` because no REST endpoint exposes the slug — the
7
+ * dashboard builds it from the App id the same way. The round-trip test pins them together.
8
+ */
9
+ export declare function encodeUuid(uuid: string): string;
10
+ export declare function decodeUuid(token: string): string;
11
+ /** The development workspace of an App: what `fcode clone` takes and `team:clone` clones per app. */
12
+ export declare const devSlug: (appId: string) => string;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace slugs embed an App's UUID base36-encoded: the platform derives a Postgres database
4
+ * name from the slug and a raw UUID overflows the 63-char identifier limit, so 128 bits become
5
+ * 25 lowercase alphanumeric characters.
6
+ *
7
+ * Ported from the dashboard's `lib/slug.ts` because no REST endpoint exposes the slug — the
8
+ * dashboard builds it from the App id the same way. The round-trip test pins them together.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.devSlug = void 0;
12
+ exports.encodeUuid = encodeUuid;
13
+ exports.decodeUuid = decodeUuid;
14
+ const TOKEN_LENGTH = 25; // ceil(128 / log2(36))
15
+ const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
16
+ function encodeUuid(uuid) {
17
+ const hex = uuid.replaceAll('-', '').toLowerCase();
18
+ if (!/^[\da-f]{32}$/.test(hex)) {
19
+ throw new Error(`Not a UUID: ${uuid}`);
20
+ }
21
+ return BigInt(`0x${hex}`).toString(36).padStart(TOKEN_LENGTH, '0');
22
+ }
23
+ function decodeUuid(token) {
24
+ let value = 0n;
25
+ for (const char of token.toLowerCase()) {
26
+ const digit = ALPHABET.indexOf(char);
27
+ if (digit === -1) {
28
+ throw new Error(`Invalid slug token: ${token}`);
29
+ }
30
+ value = value * 36n + BigInt(digit);
31
+ }
32
+ const hex = value.toString(16).padStart(32, '0');
33
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
34
+ }
35
+ /** The development workspace of an App: what `fcode clone` takes and `team:clone` clones per app. */
36
+ const devSlug = (appId) => `dev-${encodeUuid(appId)}`;
37
+ exports.devSlug = devSlug;
@@ -0,0 +1,19 @@
1
+ export declare const DEV_NULL = "/dev/null";
2
+ /**
3
+ * One comparable artifact of a resource, named after the file it is stored in. A side left
4
+ * undefined does not exist there (a process only in the cloud, a module only on disk); an empty
5
+ * string is a file that exists and is empty, which is a different thing and diffs differently.
6
+ */
7
+ export type PseudoFile = {
8
+ path: string;
9
+ remote?: string;
10
+ local?: string;
11
+ };
12
+ export declare const hasDifference: (file: PseudoFile) => boolean;
13
+ /**
14
+ * A unified diff of one pseudo-file, remote as the old side and local as the new one, so the
15
+ * patch reads as what a push would apply to the cloud. Empty when the sides are equal.
16
+ */
17
+ export declare function renderUnifiedDiff(file: PseudoFile): string;
18
+ /** Colors a rendered patch the way git does. Chalk no-ops when the output is not a TTY. */
19
+ export declare function colorizeDiff(patch: string): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasDifference = exports.DEV_NULL = void 0;
4
+ exports.renderUnifiedDiff = renderUnifiedDiff;
5
+ exports.colorizeDiff = colorizeDiff;
6
+ const diff_1 = require("diff");
7
+ const chalk = require("chalk");
8
+ exports.DEV_NULL = '/dev/null';
9
+ const hasDifference = (file) => file.remote !== file.local;
10
+ exports.hasDifference = hasDifference;
11
+ /**
12
+ * A hunk range the way git writes one: the count is left out when it is 1, and an empty range
13
+ * points at the line it would be inserted after, so a whole-file addition reads `-0,0`.
14
+ */
15
+ function formatRange(start, lines) {
16
+ if (lines === 1) {
17
+ return `${start}`;
18
+ }
19
+ return `${lines === 0 ? start - 1 : start},${lines}`;
20
+ }
21
+ /**
22
+ * A unified diff of one pseudo-file, remote as the old side and local as the new one, so the
23
+ * patch reads as what a push would apply to the cloud. Empty when the sides are equal.
24
+ */
25
+ function renderUnifiedDiff(file) {
26
+ if (!(0, exports.hasDifference)(file)) {
27
+ return '';
28
+ }
29
+ const oldName = file.remote === undefined ? exports.DEV_NULL : `remote/${file.path}`;
30
+ const newName = file.local === undefined ? exports.DEV_NULL : `local/${file.path}`;
31
+ const { hunks } = (0, diff_1.structuredPatch)(oldName, newName, file.remote ?? '', file.local ?? '', undefined, undefined, { context: 3 });
32
+ const lines = [`--- ${oldName}`, `+++ ${newName}`];
33
+ for (const hunk of hunks) {
34
+ lines.push(`@@ -${formatRange(hunk.oldStart, hunk.oldLines)} +${formatRange(hunk.newStart, hunk.newLines)} @@`, ...hunk.lines);
35
+ }
36
+ return lines.join('\n');
37
+ }
38
+ /** Colors a rendered patch the way git does. Chalk no-ops when the output is not a TTY. */
39
+ function colorizeDiff(patch) {
40
+ return patch
41
+ .split('\n')
42
+ .map((line) => {
43
+ if (line.startsWith('---') || line.startsWith('+++')) {
44
+ return chalk.bold(line);
45
+ }
46
+ if (line.startsWith('@@')) {
47
+ return chalk.cyan(line);
48
+ }
49
+ if (line.startsWith('+')) {
50
+ return chalk.green(line);
51
+ }
52
+ if (line.startsWith('-')) {
53
+ return chalk.red(line);
54
+ }
55
+ return line;
56
+ })
57
+ .join('\n');
58
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Folder name for a development team or an App, derived from its display name. Names are free
3
+ * text — accents, punctuation, emoji — while the folder has to be portable and typeable, so
4
+ * everything outside `[a-z0-9-]` collapses into a single dash.
5
+ */
6
+ export declare function slugify(name: string): string;
7
+ /**
8
+ * `slugify` with a fallback, so a name written entirely in a non-latin script still yields a
9
+ * usable folder instead of an empty one.
10
+ */
11
+ export declare function slugifyOr(name: string, fallback: string): string;
12
+ /**
13
+ * The first claimant of a slug keeps it; the next ones get `-2`, `-3`… Callers assign folders in a
14
+ * stable order (by app id), so the same team always produces the same layout — and once assigned,
15
+ * the folder is recorded in the team metadata and never derived again.
16
+ */
17
+ export declare function uniqueSlug(slug: string, taken: Set<string>): string;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.slugify = slugify;
4
+ exports.slugifyOr = slugifyOr;
5
+ exports.uniqueSlug = uniqueSlug;
6
+ /**
7
+ * Folder name for a development team or an App, derived from its display name. Names are free
8
+ * text — accents, punctuation, emoji — while the folder has to be portable and typeable, so
9
+ * everything outside `[a-z0-9-]` collapses into a single dash.
10
+ */
11
+ function slugify(name) {
12
+ return name
13
+ .normalize('NFKD')
14
+ .replaceAll(/[̀-ͯ]/g, '')
15
+ .replaceAll(/[^a-zA-Z\d]+/g, '-')
16
+ .replaceAll(/^-+|-+$/g, '')
17
+ .toLowerCase();
18
+ }
19
+ /**
20
+ * `slugify` with a fallback, so a name written entirely in a non-latin script still yields a
21
+ * usable folder instead of an empty one.
22
+ */
23
+ function slugifyOr(name, fallback) {
24
+ const slug = slugify(name);
25
+ return slug.length > 0 ? slug : fallback;
26
+ }
27
+ /**
28
+ * The first claimant of a slug keeps it; the next ones get `-2`, `-3`… Callers assign folders in a
29
+ * stable order (by app id), so the same team always produces the same layout — and once assigned,
30
+ * the folder is recorded in the team metadata and never derived again.
31
+ */
32
+ function uniqueSlug(slug, taken) {
33
+ if (!taken.has(slug)) {
34
+ return slug;
35
+ }
36
+ let suffix = 2;
37
+ while (taken.has(`${slug}-${suffix}`)) {
38
+ suffix += 1;
39
+ }
40
+ return `${slug}-${suffix}`;
41
+ }