@mondaydotcomorg/z2h-cli 0.9.1 → 0.10.1

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 (42) hide show
  1. package/dist/commands/create.d.ts +4 -0
  2. package/dist/commands/create.d.ts.map +1 -1
  3. package/dist/commands/create.js +24 -0
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +5 -1
  6. package/dist/esm/commands/create.d.ts +4 -0
  7. package/dist/esm/commands/create.d.ts.map +1 -1
  8. package/dist/esm/commands/create.mjs +24 -1
  9. package/dist/esm/commands/deploy.d.ts.map +1 -1
  10. package/dist/esm/commands/deploy.mjs +5 -1
  11. package/dist/esm/index.mjs +3 -0
  12. package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
  13. package/dist/esm/util/auth/auth-store.mjs +10 -0
  14. package/dist/esm/util/git/repo.d.ts +2 -1
  15. package/dist/esm/util/git/repo.d.ts.map +1 -1
  16. package/dist/esm/util/git/repo.mjs +24 -8
  17. package/dist/esm/util/machine-identity.d.ts +9 -0
  18. package/dist/esm/util/machine-identity.d.ts.map +1 -0
  19. package/dist/esm/util/machine-identity.mjs +12 -0
  20. package/dist/esm/util/manifest.d.ts +1 -1
  21. package/dist/esm/util/manifest.d.ts.map +1 -1
  22. package/dist/index.js +3 -0
  23. package/dist/util/auth/auth-store.d.ts.map +1 -1
  24. package/dist/util/auth/auth-store.js +10 -0
  25. package/dist/util/git/repo.d.ts +2 -1
  26. package/dist/util/git/repo.d.ts.map +1 -1
  27. package/dist/util/git/repo.js +25 -8
  28. package/dist/util/machine-identity.d.ts +9 -0
  29. package/dist/util/machine-identity.d.ts.map +1 -0
  30. package/dist/util/machine-identity.js +14 -0
  31. package/dist/util/manifest.d.ts +1 -1
  32. package/dist/util/manifest.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/commands/__tests__/create.test.ts +45 -6
  35. package/src/commands/create.ts +28 -0
  36. package/src/commands/deploy.ts +5 -1
  37. package/src/index.ts +18 -10
  38. package/src/util/auth/auth-store.ts +12 -0
  39. package/src/util/git/__tests__/repo.test.ts +57 -18
  40. package/src/util/git/repo.ts +24 -7
  41. package/src/util/machine-identity.ts +10 -0
  42. package/src/util/manifest.ts +1 -1
@@ -1,6 +1,10 @@
1
+ export declare const REMOTE_TYPES: readonly ["s3", "github"];
2
+ export type RemoteType = (typeof REMOTE_TYPES)[number];
1
3
  export interface CreateOptions {
2
4
  template?: string;
3
5
  htmlFile?: string;
6
+ remote?: RemoteType;
7
+ repoUrl?: string;
4
8
  json?: boolean;
5
9
  }
6
10
  export declare function createCommand(appName: string, opts?: CreateOptions): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6BD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8F5F"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,YAAY,2BAA4B,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6BD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoH5F"}
@@ -10,12 +10,14 @@ const util_manifest = require('../util/manifest.js');
10
10
  const util_appName = require('../util/app-name.js');
11
11
  const constants = require('../constants.js');
12
12
  const util_npm_install = require('../util/npm/install.js');
13
+ const util_git_repo = require('../util/git/repo.js');
13
14
  const commands_build = require('./build.js');
14
15
 
15
16
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
16
17
 
17
18
  const path__default = /*#__PURE__*/_interopDefault(path);
18
19
 
20
+ const REMOTE_TYPES = ['s3', 'github'];
19
21
  // Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
20
22
  // /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
21
23
  // platform page in the URL, so we refuse upfront.
@@ -54,6 +56,16 @@ async function createCommand(appName, opts = {}) {
54
56
  throw new Error(`App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
55
57
  `2-50 chars, starting with a letter.`);
56
58
  }
59
+ // Validate --remote up front before any file I/O.
60
+ if (!opts.remote) {
61
+ throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
62
+ }
63
+ if (!REMOTE_TYPES.includes(opts.remote)) {
64
+ throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
65
+ }
66
+ if (opts.remote === 'github' && !opts.repoUrl) {
67
+ throw new Error('--repo-url is required when using --remote=github.');
68
+ }
57
69
  // Resolve template.
58
70
  // `--html-file` implies `--template html-embed` unless the user already picked one.
59
71
  const template = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default'));
@@ -96,6 +108,15 @@ async function createCommand(appName, opts = {}) {
96
108
  if (template === 'html-embed' && opts.htmlFile) {
97
109
  await fs.copy(opts.htmlFile, path__default.default.join(appDir, 'src', 'page.html'));
98
110
  }
111
+ util_logger.info(`initializing git repo for ${appName}...`);
112
+ let gitRemote;
113
+ if (opts.remote === 's3') {
114
+ gitRemote = util_git_repo.defaultS3Remote(appName);
115
+ }
116
+ else {
117
+ gitRemote = opts.repoUrl;
118
+ }
119
+ await util_git_repo.initRepo(appDir, gitRemote);
99
120
  util_logger.info(`installing dependencies for ${appName}...`);
100
121
  await util_npm_install.runYarnInstall({ cwd: workspace });
101
122
  // build/dev/deploy all locate the consumer via process.cwd() — chdir before calling them.
@@ -116,7 +137,10 @@ async function createCommand(appName, opts = {}) {
116
137
  appDir,
117
138
  appName,
118
139
  template,
140
+ remote: opts.remote,
141
+ gitRemote,
119
142
  });
120
143
  }
121
144
 
145
+ exports.REMOTE_TYPES = REMOTE_TYPES;
122
146
  exports.createCommand = createCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF3E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyF3E"}
@@ -11,6 +11,8 @@ const constants = require('../constants.js');
11
11
  const util_s3_client = require('../util/s3/client.js');
12
12
  const util_auth_authStore = require('../util/auth/auth-store.js');
13
13
  const util_appName = require('../util/app-name.js');
14
+ const util_machineIdentity = require('../util/machine-identity.js');
15
+ const util_git_repo = require('../util/git/repo.js');
14
16
  const commands_build = require('./build.js');
15
17
 
16
18
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -49,12 +51,14 @@ async function deployCommand(opts = {}) {
49
51
  throw new Error(`Missing asset-manifest.json at ${assetManifestPath}.`);
50
52
  }
51
53
  const assetManifest = (await fs.readJson(assetManifestPath));
52
- const deployedBy = os__default.default.userInfo().username;
54
+ const deployedBy = util_machineIdentity.getMachineAppName() ?? os__default.default.userInfo().username;
55
+ const gitRemote = await util_git_repo.getRemoteUrl(process.cwd());
53
56
  const entry = {
54
57
  ...assetManifest,
55
58
  version,
56
59
  url,
57
60
  deployedBy,
61
+ gitRemote,
58
62
  };
59
63
  if (opts.dryRun) {
60
64
  util_logger.output(`[z2h-cli] dry-run: would upload to s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
@@ -1,6 +1,10 @@
1
+ export declare const REMOTE_TYPES: readonly ["s3", "github"];
2
+ export type RemoteType = (typeof REMOTE_TYPES)[number];
1
3
  export interface CreateOptions {
2
4
  template?: string;
3
5
  htmlFile?: string;
6
+ remote?: RemoteType;
7
+ repoUrl?: string;
4
8
  json?: boolean;
5
9
  }
6
10
  export declare function createCommand(appName: string, opts?: CreateOptions): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6BD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8F5F"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,YAAY,2BAA4B,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6BD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoH5F"}
@@ -8,8 +8,10 @@ import { readManifest } from '../util/manifest.mjs';
8
8
  import { mfAppName } from '../util/app-name.mjs';
9
9
  import { APP_NAME_PATTERN } from '../constants.mjs';
10
10
  import { runYarnInstall } from '../util/npm/install.mjs';
11
+ import { defaultS3Remote, initRepo } from '../util/git/repo.mjs';
11
12
  import { buildCommand } from './build.mjs';
12
13
 
14
+ const REMOTE_TYPES = ['s3', 'github'];
13
15
  // Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
14
16
  // /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
15
17
  // platform page in the URL, so we refuse upfront.
@@ -48,6 +50,16 @@ async function createCommand(appName, opts = {}) {
48
50
  throw new Error(`App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
49
51
  `2-50 chars, starting with a letter.`);
50
52
  }
53
+ // Validate --remote up front before any file I/O.
54
+ if (!opts.remote) {
55
+ throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
56
+ }
57
+ if (!REMOTE_TYPES.includes(opts.remote)) {
58
+ throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
59
+ }
60
+ if (opts.remote === 'github' && !opts.repoUrl) {
61
+ throw new Error('--repo-url is required when using --remote=github.');
62
+ }
51
63
  // Resolve template.
52
64
  // `--html-file` implies `--template html-embed` unless the user already picked one.
53
65
  const template = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default'));
@@ -90,6 +102,15 @@ async function createCommand(appName, opts = {}) {
90
102
  if (template === 'html-embed' && opts.htmlFile) {
91
103
  await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
92
104
  }
105
+ info(`initializing git repo for ${appName}...`);
106
+ let gitRemote;
107
+ if (opts.remote === 's3') {
108
+ gitRemote = defaultS3Remote(appName);
109
+ }
110
+ else {
111
+ gitRemote = opts.repoUrl;
112
+ }
113
+ await initRepo(appDir, gitRemote);
93
114
  info(`installing dependencies for ${appName}...`);
94
115
  await runYarnInstall({ cwd: workspace });
95
116
  // build/dev/deploy all locate the consumer via process.cwd() — chdir before calling them.
@@ -110,7 +131,9 @@ async function createCommand(appName, opts = {}) {
110
131
  appDir,
111
132
  appName,
112
133
  template,
134
+ remote: opts.remote,
135
+ gitRemote,
113
136
  });
114
137
  }
115
138
 
116
- export { createCommand };
139
+ export { REMOTE_TYPES, createCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF3E"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyF3E"}
@@ -9,6 +9,8 @@ import { BUCKET_WRITE_NAME, brokerBaseUrl, BUCKET_READ_DNS_URL } from '../consta
9
9
  import { setupS3Client } from '../util/s3/client.mjs';
10
10
  import { readZthAuth } from '../util/auth/auth-store.mjs';
11
11
  import { mfAppName, cleanAppName } from '../util/app-name.mjs';
12
+ import { getMachineAppName } from '../util/machine-identity.mjs';
13
+ import { getRemoteUrl } from '../util/git/repo.mjs';
12
14
  import { buildCommand } from './build.mjs';
13
15
 
14
16
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
@@ -42,12 +44,14 @@ async function deployCommand(opts = {}) {
42
44
  throw new Error(`Missing asset-manifest.json at ${assetManifestPath}.`);
43
45
  }
44
46
  const assetManifest = (await readJson(assetManifestPath));
45
- const deployedBy = os.userInfo().username;
47
+ const deployedBy = getMachineAppName() ?? os.userInfo().username;
48
+ const gitRemote = await getRemoteUrl(process.cwd());
46
49
  const entry = {
47
50
  ...assetManifest,
48
51
  version,
49
52
  url,
50
53
  deployedBy,
54
+ gitRemote,
51
55
  };
52
56
  if (opts.dryRun) {
53
57
  output(`[z2h-cli] dry-run: would upload to s3://${BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
@@ -135,12 +135,15 @@ async function main() {
135
135
  .description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
136
136
  .option('--template <name>', 'Template name (default | html-embed)')
137
137
  .option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
138
+ .option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
139
+ .option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
138
140
  .option('--json', 'emit structured JSON on stdout')
139
141
  .action(async (appName, opts) => {
140
142
  try {
141
143
  await trackDuration('z2h_app_created', () => createCommand(appName, opts), {
142
144
  app_name: appName,
143
145
  template: opts.template ?? 'default',
146
+ remote: opts.remote ?? 'unknown',
144
147
  });
145
148
  }
146
149
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CASpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAOpD"}
1
+ {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAkBpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAUpD"}
@@ -16,6 +16,13 @@ class Z2hAuthMissingError extends Error {
16
16
  }
17
17
  }
18
18
  async function readZthAuth() {
19
+ if (process.env.Z2H_AUTH_TOKEN) {
20
+ const token = process.env.Z2H_AUTH_TOKEN;
21
+ if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
22
+ throw new Error('Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.');
23
+ }
24
+ return { token };
25
+ }
19
26
  if (!(await pathExists(Z2H_AUTH_FILE))) {
20
27
  throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
21
28
  }
@@ -35,6 +42,9 @@ async function readZthAuth() {
35
42
  * blow up — and where an inline sync read keeps the spawn call site simple.
36
43
  */
37
44
  function tryReadAuthTokenSync() {
45
+ if (process.env.Z2H_AUTH_TOKEN) {
46
+ return process.env.Z2H_AUTH_TOKEN;
47
+ }
38
48
  try {
39
49
  const parsed = readJsonSync(Z2H_AUTH_FILE);
40
50
  return parsed?.token ?? null;
@@ -2,7 +2,8 @@ export declare class GitConflictError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
4
  export declare function defaultS3Remote(app: string): string;
5
- export declare function initAndPush(dir: string, remote: string): Promise<void>;
5
+ export declare function initRepo(dir: string, remote: string): Promise<void>;
6
+ export declare function getRemoteUrl(dir: string): Promise<string | undefined>;
6
7
  export declare function cloneRepo(remote: string, dir: string): Promise<void>;
7
8
  export declare function safeBackupIfDirty(dir: string): Promise<void>;
8
9
  export declare function pullAndResolve(dir: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../../src/util/git/repo.ts"],"names":[],"mappings":"AAKA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvF"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../../src/util/git/repo.ts"],"names":[],"mappings":"AAMA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvF"}
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { execa } from 'execa';
4
4
  import { BUCKET_WRITE_NAME } from '../../constants.mjs';
5
+ import { mfAppName } from '../app-name.mjs';
5
6
 
6
7
  class GitConflictError extends Error {
7
8
  constructor(message) {
@@ -11,15 +12,19 @@ class GitConflictError extends Error {
11
12
  }
12
13
  const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
13
14
  function defaultS3Remote(app) {
14
- return `s3://${BUCKET_WRITE_NAME}/${app}/git`;
15
+ return `s3://${BUCKET_WRITE_NAME}/${mfAppName(app)}/git`;
15
16
  }
16
- async function initAndPush(dir, remote) {
17
+ async function initRepo(dir, remote) {
17
18
  await ensureGitignore(dir);
18
19
  await execa('git', ['init', '-b', 'master'], { cwd: dir });
19
20
  await execa('git', ['add', '.'], { cwd: dir });
20
21
  await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
21
22
  await execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
22
- await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
23
+ }
24
+ async function getRemoteUrl(dir) {
25
+ return execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
26
+ .then(r => r.stdout.trim() || undefined)
27
+ .catch(() => undefined);
23
28
  }
24
29
  async function cloneRepo(remote, dir) {
25
30
  await execa('git', ['clone', remote, dir]);
@@ -62,14 +67,25 @@ async function localHead(dir) {
62
67
  return stdout.trim();
63
68
  }
64
69
  async function commitDetailedAndPush(dir, version) {
65
- const message = await buildCommitMessage(dir, version);
70
+ const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
71
+ if (branch.trim() !== 'master') {
72
+ throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
73
+ }
66
74
  await execa('git', ['add', '.'], { cwd: dir });
75
+ const { stdout: staged } = await execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
76
+ if (!staged.trim()) {
77
+ throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
78
+ }
79
+ const message = await buildCommitMessage(dir, version);
67
80
  await execa('git', ['commit', '-m', message], { cwd: dir });
68
81
  try {
69
- await execa('git', ['push'], { cwd: dir });
82
+ await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
70
83
  }
71
- catch {
72
- throw new GitConflictError('push rejected — remote has new commits');
84
+ catch (err) {
85
+ if (/\[rejected\]|non-fast-forward/.test(err.message)) {
86
+ throw new GitConflictError('push rejected — remote has new commits');
87
+ }
88
+ throw err;
73
89
  }
74
90
  }
75
91
  async function buildCommitMessage(dir, version) {
@@ -102,4 +118,4 @@ async function ensureGitignore(dir) {
102
118
  await fs.outputFile(gitignorePath, content + separator + missing.join('\n') + '\n');
103
119
  }
104
120
 
105
- export { GitConflictError, cloneRepo, commitDetailedAndPush, defaultS3Remote, initAndPush, localHead, pullAndResolve, remoteHead, safeBackupIfDirty };
121
+ export { GitConflictError, cloneRepo, commitDetailedAndPush, defaultS3Remote, getRemoteUrl, initRepo, localHead, pullAndResolve, remoteHead, safeBackupIfDirty };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns the machine app name when running as a service (e.g. agent-runner).
3
+ * Available when the process is a monday.com microservice with `APP_NAME` set
4
+ * by the platform runtime.
5
+ *
6
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
7
+ */
8
+ export declare function getMachineAppName(): string | undefined;
9
+ //# sourceMappingURL=machine-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns the machine app name when running as a service (e.g. agent-runner).
3
+ * Available when the process is a monday.com microservice with `APP_NAME` set
4
+ * by the platform runtime.
5
+ *
6
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
7
+ */
8
+ function getMachineAppName() {
9
+ return process.env.APP_NAME || undefined;
10
+ }
11
+
12
+ export { getMachineAppName };
@@ -18,12 +18,12 @@ export interface AssetManifestFile {
18
18
  nonExternalsEntrypoints?: string[];
19
19
  scriptType?: 'module' | 'classic';
20
20
  mfs?: string[];
21
- gitRemote: string;
22
21
  }
23
22
  export interface ManifestEntry extends AssetManifestFile {
24
23
  version: number;
25
24
  url: string;
26
25
  deployedBy: string;
26
+ gitRemote?: string;
27
27
  }
28
28
  export type Manifest = Record<string, ManifestEntry>;
29
29
  export declare function readManifest(): Promise<Manifest>;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
package/dist/index.js CHANGED
@@ -141,12 +141,15 @@ async function main() {
141
141
  .description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
142
142
  .option('--template <name>', 'Template name (default | html-embed)')
143
143
  .option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
144
+ .option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
145
+ .option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
144
146
  .option('--json', 'emit structured JSON on stdout')
145
147
  .action(async (appName, opts) => {
146
148
  try {
147
149
  await util_tracker.trackDuration('z2h_app_created', () => commands_create.createCommand(appName, opts), {
148
150
  app_name: appName,
149
151
  template: opts.template ?? 'default',
152
+ remote: opts.remote ?? 'unknown',
150
153
  });
151
154
  }
152
155
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CASpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAOpD"}
1
+ {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAkBpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAUpD"}
@@ -23,6 +23,13 @@ class Z2hAuthMissingError extends Error {
23
23
  }
24
24
  }
25
25
  async function readZthAuth() {
26
+ if (process.env.Z2H_AUTH_TOKEN) {
27
+ const token = process.env.Z2H_AUTH_TOKEN;
28
+ if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
29
+ throw new Error('Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.');
30
+ }
31
+ return { token };
32
+ }
26
33
  if (!(await fs.pathExists(Z2H_AUTH_FILE))) {
27
34
  throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
28
35
  }
@@ -42,6 +49,9 @@ async function readZthAuth() {
42
49
  * blow up — and where an inline sync read keeps the spawn call site simple.
43
50
  */
44
51
  function tryReadAuthTokenSync() {
52
+ if (process.env.Z2H_AUTH_TOKEN) {
53
+ return process.env.Z2H_AUTH_TOKEN;
54
+ }
45
55
  try {
46
56
  const parsed = fs.readJsonSync(Z2H_AUTH_FILE);
47
57
  return parsed?.token ?? null;
@@ -2,7 +2,8 @@ export declare class GitConflictError extends Error {
2
2
  constructor(message: string);
3
3
  }
4
4
  export declare function defaultS3Remote(app: string): string;
5
- export declare function initAndPush(dir: string, remote: string): Promise<void>;
5
+ export declare function initRepo(dir: string, remote: string): Promise<void>;
6
+ export declare function getRemoteUrl(dir: string): Promise<string | undefined>;
6
7
  export declare function cloneRepo(remote: string, dir: string): Promise<void>;
7
8
  export declare function safeBackupIfDirty(dir: string): Promise<void>;
8
9
  export declare function pullAndResolve(dir: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAKA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvF"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAMA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvF"}
@@ -4,6 +4,7 @@ const path = require('node:path');
4
4
  const fs = require('fs-extra');
5
5
  const execa = require('execa');
6
6
  const constants = require('../../constants.js');
7
+ const util_appName = require('../app-name.js');
7
8
 
8
9
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
9
10
 
@@ -18,15 +19,19 @@ class GitConflictError extends Error {
18
19
  }
19
20
  const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
20
21
  function defaultS3Remote(app) {
21
- return `s3://${constants.BUCKET_WRITE_NAME}/${app}/git`;
22
+ return `s3://${constants.BUCKET_WRITE_NAME}/${util_appName.mfAppName(app)}/git`;
22
23
  }
23
- async function initAndPush(dir, remote) {
24
+ async function initRepo(dir, remote) {
24
25
  await ensureGitignore(dir);
25
26
  await execa.execa('git', ['init', '-b', 'master'], { cwd: dir });
26
27
  await execa.execa('git', ['add', '.'], { cwd: dir });
27
28
  await execa.execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
28
29
  await execa.execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
29
- await execa.execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
30
+ }
31
+ async function getRemoteUrl(dir) {
32
+ return execa.execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
33
+ .then(r => r.stdout.trim() || undefined)
34
+ .catch(() => undefined);
30
35
  }
31
36
  async function cloneRepo(remote, dir) {
32
37
  await execa.execa('git', ['clone', remote, dir]);
@@ -69,14 +74,25 @@ async function localHead(dir) {
69
74
  return stdout.trim();
70
75
  }
71
76
  async function commitDetailedAndPush(dir, version) {
72
- const message = await buildCommitMessage(dir, version);
77
+ const { stdout: branch } = await execa.execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
78
+ if (branch.trim() !== 'master') {
79
+ throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
80
+ }
73
81
  await execa.execa('git', ['add', '.'], { cwd: dir });
82
+ const { stdout: staged } = await execa.execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
83
+ if (!staged.trim()) {
84
+ throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
85
+ }
86
+ const message = await buildCommitMessage(dir, version);
74
87
  await execa.execa('git', ['commit', '-m', message], { cwd: dir });
75
88
  try {
76
- await execa.execa('git', ['push'], { cwd: dir });
89
+ await execa.execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
77
90
  }
78
- catch {
79
- throw new GitConflictError('push rejected — remote has new commits');
91
+ catch (err) {
92
+ if (/\[rejected\]|non-fast-forward/.test(err.message)) {
93
+ throw new GitConflictError('push rejected — remote has new commits');
94
+ }
95
+ throw err;
80
96
  }
81
97
  }
82
98
  async function buildCommitMessage(dir, version) {
@@ -113,7 +129,8 @@ exports.GitConflictError = GitConflictError;
113
129
  exports.cloneRepo = cloneRepo;
114
130
  exports.commitDetailedAndPush = commitDetailedAndPush;
115
131
  exports.defaultS3Remote = defaultS3Remote;
116
- exports.initAndPush = initAndPush;
132
+ exports.getRemoteUrl = getRemoteUrl;
133
+ exports.initRepo = initRepo;
117
134
  exports.localHead = localHead;
118
135
  exports.pullAndResolve = pullAndResolve;
119
136
  exports.remoteHead = remoteHead;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns the machine app name when running as a service (e.g. agent-runner).
3
+ * Available when the process is a monday.com microservice with `APP_NAME` set
4
+ * by the platform runtime.
5
+ *
6
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
7
+ */
8
+ export declare function getMachineAppName(): string | undefined;
9
+ //# sourceMappingURL=machine-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
@@ -0,0 +1,14 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ /**
4
+ * Returns the machine app name when running as a service (e.g. agent-runner).
5
+ * Available when the process is a monday.com microservice with `APP_NAME` set
6
+ * by the platform runtime.
7
+ *
8
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
9
+ */
10
+ function getMachineAppName() {
11
+ return process.env.APP_NAME || undefined;
12
+ }
13
+
14
+ exports.getMachineAppName = getMachineAppName;
@@ -18,12 +18,12 @@ export interface AssetManifestFile {
18
18
  nonExternalsEntrypoints?: string[];
19
19
  scriptType?: 'module' | 'classic';
20
20
  mfs?: string[];
21
- gitRemote: string;
22
21
  }
23
22
  export interface ManifestEntry extends AssetManifestFile {
24
23
  version: number;
25
24
  url: string;
26
25
  deployedBy: string;
26
+ gitRemote?: string;
27
27
  }
28
28
  export type Manifest = Record<string, ManifestEntry>;
29
29
  export declare function readManifest(): Promise<Manifest>;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -4,6 +4,7 @@ import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists, writeFile } fr
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import { runYarnInstall } from '../../util/npm/install';
7
+ import { initRepo, defaultS3Remote } from '../../util/git/repo';
7
8
  import { buildCommand } from '../build';
8
9
  import { createCommand } from '../create';
9
10
  import * as manifest from '../../util/manifest';
@@ -15,6 +16,11 @@ vi.mock('../../util/npm/install', () => ({
15
16
  runYarnInstall: vi.fn().mockResolvedValue(undefined),
16
17
  }));
17
18
 
19
+ vi.mock('../../util/git/repo', () => ({
20
+ initRepo: vi.fn().mockResolvedValue(undefined),
21
+ defaultS3Remote: vi.fn((app: string) => `s3://prod-use1-bigbrain-zth-mf-assets/mf-${app}/git`),
22
+ }));
23
+
18
24
  // Mock build so tsc/yarn build don't run in tests.
19
25
  vi.mock('../build', () => ({
20
26
  buildCommand: vi.fn().mockResolvedValue(undefined),
@@ -39,6 +45,8 @@ describe('create', () => {
39
45
  tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-c-'));
40
46
  process.env.Z2H_WORKSPACE_HOME = tmp;
41
47
  vi.mocked(runYarnInstall).mockClear();
48
+ vi.mocked(initRepo).mockClear();
49
+ vi.mocked(defaultS3Remote).mockClear();
42
50
  vi.mocked(buildCommand).mockClear();
43
51
  });
44
52
 
@@ -63,12 +71,12 @@ describe('create', () => {
63
71
  const appDir = path.join(tmp, 'demo-app');
64
72
  await ensureDir(appDir);
65
73
  await writeFile(path.join(appDir, 'something.txt'), 'hi');
66
- await expect(createCommand('demo-app')).rejects.toThrow(/non-empty/);
74
+ await expect(createCommand('demo-app', { remote: 's3' })).rejects.toThrow(/non-empty/);
67
75
  });
68
76
 
69
77
  it('scaffolds an app with the default React template', async () => {
70
78
  await makeWorkspace(tmp);
71
- await createCommand('demo-app');
79
+ await createCommand('demo-app', { remote: 's3' });
72
80
 
73
81
  const appDir = path.join(tmp, 'demo-app');
74
82
  expect(await pathExists(path.join(appDir, 'package.json'))).toBe(true);
@@ -94,7 +102,7 @@ describe('create', () => {
94
102
  const htmlPath = path.join(tmp, 'page.html');
95
103
  await writeFile(htmlPath, '<html><body>Hi</body></html>');
96
104
 
97
- await createCommand('html-app', { template: 'html-embed', htmlFile: htmlPath });
105
+ await createCommand('html-app', { template: 'html-embed', htmlFile: htmlPath, remote: 's3' });
98
106
 
99
107
  const appDir = path.join(tmp, 'html-app');
100
108
  expect(await pathExists(path.join(appDir, 'src', 'page.html'))).toBe(true);
@@ -107,13 +115,44 @@ describe('create', () => {
107
115
 
108
116
  it('rejects unknown templates', async () => {
109
117
  await makeWorkspace(tmp);
110
- await expect(createCommand('demo', { template: 'nonexistent' })).rejects.toThrow(/unknown template/i);
118
+ await expect(createCommand('demo', { remote: 's3', template: 'nonexistent' })).rejects.toThrow(/unknown template/i);
111
119
  });
112
120
 
113
121
  it('rejects --html-file when the file does not exist', async () => {
114
122
  await makeWorkspace(tmp);
115
- await expect(createCommand('demo', { template: 'html-embed', htmlFile: '/no/such/file.html' })).rejects.toThrow(
116
- /html file/i
123
+ await expect(
124
+ createCommand('demo', { remote: 's3', template: 'html-embed', htmlFile: '/no/such/file.html' })
125
+ ).rejects.toThrow(/html file/i);
126
+ });
127
+
128
+ it('rejects when --remote is missing', async () => {
129
+ await makeWorkspace(tmp);
130
+ await expect(createCommand('demo-app', {})).rejects.toThrow(/--remote is required/);
131
+ });
132
+
133
+ it('rejects an unknown --remote value', async () => {
134
+ await makeWorkspace(tmp);
135
+ await expect(createCommand('demo-app', { remote: 'bitbucket' as 's3' })).rejects.toThrow(/Unknown remote/);
136
+ });
137
+
138
+ it('rejects --remote=github without --repo-url', async () => {
139
+ await makeWorkspace(tmp);
140
+ await expect(createCommand('demo-app', { remote: 'github' })).rejects.toThrow(/--repo-url is required/);
141
+ });
142
+
143
+ it('calls initRepo with the S3 remote for --remote=s3', async () => {
144
+ await makeWorkspace(tmp);
145
+ await createCommand('my-app', { remote: 's3' });
146
+ expect(vi.mocked(defaultS3Remote)).toHaveBeenCalledWith('my-app');
147
+ expect(vi.mocked(initRepo)).toHaveBeenCalledWith(
148
+ path.join(tmp, 'my-app'),
149
+ 's3://prod-use1-bigbrain-zth-mf-assets/mf-my-app/git'
117
150
  );
118
151
  });
152
+
153
+ it('calls initRepo with the repo URL for --remote=github', async () => {
154
+ await makeWorkspace(tmp);
155
+ await createCommand('gh-app', { remote: 'github', repoUrl: 'https://github.com/acme/gh-app' });
156
+ expect(vi.mocked(initRepo)).toHaveBeenCalledWith(path.join(tmp, 'gh-app'), 'https://github.com/acme/gh-app');
157
+ });
119
158
  });
@@ -8,11 +8,17 @@ import { readManifest } from '../util/manifest';
8
8
  import { mfAppName } from '../util/app-name';
9
9
  import { APP_NAME_PATTERN } from '../constants';
10
10
  import { runYarnInstall } from '../util/npm/install';
11
+ import { initRepo, defaultS3Remote } from '../util/git/repo';
11
12
  import { buildCommand } from './build';
12
13
 
14
+ export const REMOTE_TYPES = ['s3', 'github'] as const;
15
+ export type RemoteType = (typeof REMOTE_TYPES)[number];
16
+
13
17
  export interface CreateOptions {
14
18
  template?: string;
15
19
  htmlFile?: string;
20
+ remote?: RemoteType;
21
+ repoUrl?: string;
16
22
  json?: boolean;
17
23
  }
18
24
 
@@ -64,6 +70,17 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
64
70
  );
65
71
  }
66
72
 
73
+ // Validate --remote up front before any file I/O.
74
+ if (!opts.remote) {
75
+ throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
76
+ }
77
+ if (!(REMOTE_TYPES as readonly string[]).includes(opts.remote)) {
78
+ throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
79
+ }
80
+ if (opts.remote === 'github' && !opts.repoUrl) {
81
+ throw new Error('--repo-url is required when using --remote=github.');
82
+ }
83
+
67
84
  // Resolve template.
68
85
  // `--html-file` implies `--template html-embed` unless the user already picked one.
69
86
  const template: AppTemplate = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default')) as AppTemplate;
@@ -116,6 +133,15 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
116
133
  await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
117
134
  }
118
135
 
136
+ info(`initializing git repo for ${appName}...`);
137
+ let gitRemote: string;
138
+ if (opts.remote === 's3') {
139
+ gitRemote = defaultS3Remote(appName);
140
+ } else {
141
+ gitRemote = opts.repoUrl as string;
142
+ }
143
+ await initRepo(appDir, gitRemote);
144
+
119
145
  info(`installing dependencies for ${appName}...`);
120
146
  await runYarnInstall({ cwd: workspace });
121
147
 
@@ -136,5 +162,7 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
136
162
  appDir,
137
163
  appName,
138
164
  template,
165
+ remote: opts.remote,
166
+ gitRemote,
139
167
  });
140
168
  }
@@ -16,6 +16,8 @@ import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL, brokerBaseUrl } from '../consta
16
16
  import { setupS3Client } from '../util/s3/client';
17
17
  import { readZthAuth } from '../util/auth/auth-store';
18
18
  import { cleanAppName, mfAppName } from '../util/app-name';
19
+ import { getMachineAppName } from '../util/machine-identity';
20
+ import { getRemoteUrl } from '../util/git/repo';
19
21
  import { buildCommand } from './build';
20
22
 
21
23
  export interface DeployOptions {
@@ -61,12 +63,14 @@ export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
61
63
  }
62
64
  const assetManifest = (await readJson(assetManifestPath)) as AssetManifestFile;
63
65
 
64
- const deployedBy = os.userInfo().username;
66
+ const deployedBy = getMachineAppName() ?? os.userInfo().username;
67
+ const gitRemote = await getRemoteUrl(process.cwd());
65
68
  const entry: ManifestEntry = {
66
69
  ...assetManifest,
67
70
  version,
68
71
  url,
69
72
  deployedBy,
73
+ gitRemote,
70
74
  };
71
75
 
72
76
  if (opts.dryRun) {
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ import { buildCommand } from './commands/build';
9
9
  import { cleanCommand } from './commands/clean';
10
10
  import { deployCommand } from './commands/deploy';
11
11
  import { createWorkspaceCommand } from './commands/create-workspace';
12
- import { createCommand } from './commands/create';
12
+ import { createCommand, CreateOptions } from './commands/create';
13
13
  import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
14
14
  import { migrateCommand } from './commands/migrate';
15
15
  import { errorLog, fail, getOutputMode } from './util/logger';
@@ -142,17 +142,25 @@ async function main(): Promise<void> {
142
142
  .description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
143
143
  .option('--template <name>', 'Template name (default | html-embed)')
144
144
  .option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
145
+ .option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
146
+ .option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
145
147
  .option('--json', 'emit structured JSON on stdout')
146
- .action(async (appName: string, opts: { template?: string; htmlFile?: string; json?: boolean }) => {
147
- try {
148
- await trackDuration('z2h_app_created', () => createCommand(appName, opts), {
149
- app_name: appName,
150
- template: opts.template ?? 'default',
151
- });
152
- } catch (err) {
153
- await fail(`create failed: ${(err as Error).message}`, { command: 'create' }, 1);
148
+ .action(
149
+ async (
150
+ appName: string,
151
+ opts: { template?: string; htmlFile?: string; remote?: string; repoUrl?: string; json?: boolean }
152
+ ) => {
153
+ try {
154
+ await trackDuration('z2h_app_created', () => createCommand(appName, opts as CreateOptions), {
155
+ app_name: appName,
156
+ template: opts.template ?? 'default',
157
+ remote: opts.remote ?? 'unknown',
158
+ });
159
+ } catch (err) {
160
+ await fail(`create failed: ${(err as Error).message}`, { command: 'create' }, 1);
161
+ }
154
162
  }
155
- });
163
+ );
156
164
 
157
165
  program
158
166
  .command('migrate')
@@ -20,6 +20,15 @@ export class Z2hAuthMissingError extends Error {
20
20
  }
21
21
 
22
22
  export async function readZthAuth(): Promise<ZthAuth> {
23
+ if (process.env.Z2H_AUTH_TOKEN) {
24
+ const token = process.env.Z2H_AUTH_TOKEN;
25
+ if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
26
+ throw new Error(
27
+ 'Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.'
28
+ );
29
+ }
30
+ return { token };
31
+ }
23
32
  if (!(await pathExists(Z2H_AUTH_FILE))) {
24
33
  throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
25
34
  }
@@ -40,6 +49,9 @@ export async function readZthAuth(): Promise<ZthAuth> {
40
49
  * blow up — and where an inline sync read keeps the spawn call site simple.
41
50
  */
42
51
  export function tryReadAuthTokenSync(): string | null {
52
+ if (process.env.Z2H_AUTH_TOKEN) {
53
+ return process.env.Z2H_AUTH_TOKEN;
54
+ }
43
55
  try {
44
56
  const parsed = readJsonSync(Z2H_AUTH_FILE) as Partial<ZthAuth> | null;
45
57
  return parsed?.token ?? null;
@@ -6,13 +6,13 @@ import fs from 'fs-extra';
6
6
  import {
7
7
  GitConflictError,
8
8
  defaultS3Remote,
9
+ initRepo,
9
10
  cloneRepo,
10
11
  safeBackupIfDirty,
11
12
  pullAndResolve,
12
13
  remoteHead,
13
14
  localHead,
14
15
  commitDetailedAndPush,
15
- initAndPush,
16
16
  } from '../repo';
17
17
 
18
18
  vi.mock('execa', () => ({ execa: vi.fn() }));
@@ -42,7 +42,7 @@ beforeEach(() => {
42
42
 
43
43
  describe('defaultS3Remote', () => {
44
44
  it('returns the expected S3 URL', () => {
45
- expect(defaultS3Remote('my-app')).toBe('s3://prod-use1-bigbrain-zth-mf-assets/my-app/git');
45
+ expect(defaultS3Remote('my-app')).toBe('s3://prod-use1-bigbrain-zth-mf-assets/mf-my-app/git');
46
46
  });
47
47
  });
48
48
 
@@ -56,27 +56,27 @@ describe('cloneRepo', () => {
56
56
  });
57
57
  });
58
58
 
59
- // ── initAndPush ──────────────────────────────────────────────────────────────
59
+ // ── initRepo ─────────────────────────────────────────────────────────────────
60
60
 
61
- describe('initAndPush', () => {
62
- it('runs the correct git command sequence', async () => {
61
+ describe('initRepo', () => {
62
+ it('runs the correct git command sequence without pushing', async () => {
63
63
  mockExeca.mockResolvedValue(execaOk());
64
64
 
65
- await initAndPush(DIR, 's3://bucket/my-app/git');
65
+ await initRepo(DIR, 's3://bucket/my-app/git');
66
66
 
67
67
  const calls = mockExeca.mock.calls.map(c => c.slice(0, 2));
68
68
  expect(calls).toContainEqual(['git', ['init', '-b', 'master']]);
69
69
  expect(calls).toContainEqual(['git', ['add', '.']]);
70
70
  expect(calls).toContainEqual(['git', ['commit', '-m', 'Initial commit']]);
71
71
  expect(calls).toContainEqual(['git', ['remote', 'add', 'origin', 's3://bucket/my-app/git']]);
72
- expect(calls).toContainEqual(['git', ['push', '-u', 'origin', 'master']]);
72
+ expect(calls).not.toContainEqual(['git', expect.arrayContaining(['push'])]);
73
73
  });
74
74
 
75
75
  it('writes missing gitignore entries', async () => {
76
76
  mockFs.readFile.mockResolvedValueOnce('' as never);
77
77
  mockExeca.mockResolvedValue(execaOk());
78
78
 
79
- await initAndPush(DIR, 's3://bucket/my-app/git');
79
+ await initRepo(DIR, 's3://bucket/my-app/git');
80
80
 
81
81
  expect(mockFs.outputFile).toHaveBeenCalledWith(
82
82
  path.join(DIR, '.gitignore'),
@@ -88,7 +88,7 @@ describe('initAndPush', () => {
88
88
  mockFs.readFile.mockResolvedValueOnce('node_modules/\n.zth/\nbuild/\n' as never);
89
89
  mockExeca.mockResolvedValue(execaOk());
90
90
 
91
- await initAndPush(DIR, 's3://bucket/my-app/git');
91
+ await initRepo(DIR, 's3://bucket/my-app/git');
92
92
 
93
93
  expect(mockFs.outputFile).not.toHaveBeenCalled();
94
94
  });
@@ -165,12 +165,20 @@ describe('localHead', () => {
165
165
  // ── commitDetailedAndPush ────────────────────────────────────────────────────
166
166
 
167
167
  describe('commitDetailedAndPush', () => {
168
+ it('throws when not on master branch', async () => {
169
+ mockExeca.mockResolvedValueOnce(execaOk('feature/oops\n')); // git rev-parse --abbrev-ref HEAD
170
+
171
+ await expect(commitDetailedAndPush(DIR, 1)).rejects.toThrow(/expected branch "master"/);
172
+ });
173
+
168
174
  it('commits with a version-prefixed message and pushes', async () => {
169
175
  mockExeca
170
- .mockResolvedValueOnce(execaOk(' M src/index.ts\n?? new-file.ts')) // git status --short (XY + space prefix)
176
+ .mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
171
177
  .mockResolvedValueOnce(execaOk()) // git add .
178
+ .mockResolvedValueOnce(execaOk('src/index.ts\nnew-file.ts')) // git diff --cached --name-only
179
+ .mockResolvedValueOnce(execaOk('M src/index.ts\nA new-file.ts')) // git status --short
172
180
  .mockResolvedValueOnce(execaOk()) // git commit
173
- .mockResolvedValueOnce(execaOk()); // git push
181
+ .mockResolvedValueOnce(execaOk()); // git push -u origin master
174
182
 
175
183
  await commitDetailedAndPush(DIR, 3);
176
184
 
@@ -179,25 +187,56 @@ describe('commitDetailedAndPush', () => {
179
187
  const msg = (commitCall![1] as string[])[2];
180
188
  expect(msg).toMatch(/^v3:/);
181
189
  expect(msg).toContain('src/index.ts');
190
+
191
+ const pushCall = mockExeca.mock.calls.find(c => c[0] === 'git' && (c[1] as string[])[0] === 'push');
192
+ expect(pushCall![1]).toEqual(['push', '-u', 'origin', 'master']);
193
+ });
194
+
195
+ it('throws when there is nothing to commit', async () => {
196
+ mockExeca
197
+ .mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
198
+ .mockResolvedValueOnce(execaOk()) // git add .
199
+ .mockResolvedValueOnce(execaOk('')); // git diff --cached --name-only (empty)
200
+
201
+ await expect(commitDetailedAndPush(DIR, 4)).rejects.toThrow(/Nothing new to deploy/);
182
202
  });
183
203
 
184
204
  it('throws GitConflictError when push is rejected', async () => {
185
205
  mockExeca
186
- .mockResolvedValueOnce(execaOk('')) // git status --short
206
+ .mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
187
207
  .mockResolvedValueOnce(execaOk()) // git add .
208
+ .mockResolvedValueOnce(execaOk('src/index.ts')) // git diff --cached --name-only
209
+ .mockResolvedValueOnce(execaOk(' M src/index.ts')) // git status --short
188
210
  .mockResolvedValueOnce(execaOk()) // git commit
189
- .mockRejectedValueOnce(new Error('[rejected]')); // git push
211
+ .mockRejectedValueOnce(new Error('[rejected]')); // git push -u origin master
190
212
 
191
213
  await expect(commitDetailedAndPush(DIR, 4)).rejects.toThrow(GitConflictError);
192
214
  });
193
215
 
216
+ it('re-throws non-rejection push errors as-is', async () => {
217
+ mockExeca
218
+ .mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
219
+ .mockResolvedValueOnce(execaOk()) // git add .
220
+ .mockResolvedValueOnce(execaOk('src/index.ts')) // git diff --cached --name-only
221
+ .mockResolvedValueOnce(execaOk(' M src/index.ts')) // git status --short
222
+ .mockResolvedValueOnce(execaOk()) // git commit
223
+ .mockRejectedValueOnce(new Error('Connection refused')); // git push — auth/network error
224
+
225
+ const err = await commitDetailedAndPush(DIR, 4).catch(e => e);
226
+ expect(err).not.toBeInstanceOf(GitConflictError);
227
+ expect(err.message).toMatch(/Connection refused/);
228
+ });
229
+
194
230
  it('truncates long file lists to 5 entries', async () => {
195
- const manyFiles = Array.from({ length: 8 }, (_, i) => ` M file${i}.ts`).join('\n');
231
+ const manyFiles = Array.from({ length: 8 }, (_, i) => `M file${i}.ts`).join('\n');
232
+ const manyNames = Array.from({ length: 8 }, (_, i) => `file${i}.ts`).join('\n');
196
233
  mockExeca
197
- .mockResolvedValueOnce(execaOk(manyFiles))
198
- .mockResolvedValueOnce(execaOk())
199
- .mockResolvedValueOnce(execaOk())
200
- .mockResolvedValueOnce(execaOk());
234
+ .mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
235
+ .mockResolvedValueOnce(execaOk()) // git add .
236
+ .mockResolvedValueOnce(execaOk(manyNames)) // git diff --cached --name-only
237
+ .mockResolvedValueOnce(execaOk(manyFiles)) // git status --short
238
+ .mockResolvedValueOnce(execaOk()) // git commit
239
+ .mockResolvedValueOnce(execaOk()); // git push
201
240
 
202
241
  await commitDetailedAndPush(DIR, 1);
203
242
 
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { execa } from 'execa';
4
4
  import { BUCKET_WRITE_NAME } from '../../constants';
5
+ import { mfAppName } from '../app-name';
5
6
 
6
7
  export class GitConflictError extends Error {
7
8
  constructor(message: string) {
@@ -13,16 +14,21 @@ export class GitConflictError extends Error {
13
14
  const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
14
15
 
15
16
  export function defaultS3Remote(app: string): string {
16
- return `s3://${BUCKET_WRITE_NAME}/${app}/git`;
17
+ return `s3://${BUCKET_WRITE_NAME}/${mfAppName(app)}/git`;
17
18
  }
18
19
 
19
- export async function initAndPush(dir: string, remote: string): Promise<void> {
20
+ export async function initRepo(dir: string, remote: string): Promise<void> {
20
21
  await ensureGitignore(dir);
21
22
  await execa('git', ['init', '-b', 'master'], { cwd: dir });
22
23
  await execa('git', ['add', '.'], { cwd: dir });
23
24
  await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
24
25
  await execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
25
- await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
26
+ }
27
+
28
+ export async function getRemoteUrl(dir: string): Promise<string | undefined> {
29
+ return execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
30
+ .then(r => r.stdout.trim() || undefined)
31
+ .catch(() => undefined);
26
32
  }
27
33
 
28
34
  export async function cloneRepo(remote: string, dir: string): Promise<void> {
@@ -71,13 +77,24 @@ export async function localHead(dir: string): Promise<string> {
71
77
  }
72
78
 
73
79
  export async function commitDetailedAndPush(dir: string, version: number): Promise<void> {
74
- const message = await buildCommitMessage(dir, version);
80
+ const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
81
+ if (branch.trim() !== 'master') {
82
+ throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
83
+ }
75
84
  await execa('git', ['add', '.'], { cwd: dir });
85
+ const { stdout: staged } = await execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
86
+ if (!staged.trim()) {
87
+ throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
88
+ }
89
+ const message = await buildCommitMessage(dir, version);
76
90
  await execa('git', ['commit', '-m', message], { cwd: dir });
77
91
  try {
78
- await execa('git', ['push'], { cwd: dir });
79
- } catch {
80
- throw new GitConflictError('push rejected — remote has new commits');
92
+ await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
93
+ } catch (err) {
94
+ if (/\[rejected\]|non-fast-forward/.test((err as Error).message)) {
95
+ throw new GitConflictError('push rejected — remote has new commits');
96
+ }
97
+ throw err;
81
98
  }
82
99
  }
83
100
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Returns the machine app name when running as a service (e.g. agent-runner).
3
+ * Available when the process is a monday.com microservice with `APP_NAME` set
4
+ * by the platform runtime.
5
+ *
6
+ * Returns `undefined` for interactive human users (fallback to os.userInfo).
7
+ */
8
+ export function getMachineAppName(): string | undefined {
9
+ return process.env.APP_NAME || undefined;
10
+ }
@@ -23,13 +23,13 @@ export interface AssetManifestFile {
23
23
  nonExternalsEntrypoints?: string[];
24
24
  scriptType?: 'module' | 'classic';
25
25
  mfs?: string[];
26
- gitRemote: string;
27
26
  }
28
27
 
29
28
  export interface ManifestEntry extends AssetManifestFile {
30
29
  version: number;
31
30
  url: string;
32
31
  deployedBy: string;
32
+ gitRemote?: string;
33
33
  }
34
34
 
35
35
  export type Manifest = Record<string, ManifestEntry>;